diff --git a/bin/log-performance-results.js b/bin/log-performance-results.js index 861e09bbc6c4d..f18e40fea3d2f 100755 --- a/bin/log-performance-results.js +++ b/bin/log-performance-results.js @@ -13,6 +13,10 @@ const resultsFiles = [ file: 'post-editor.performance-results.json', metricsPrefix: '', }, + { + file: 'site-editor.performance-results.json', + metricsPrefix: 'site-editor-', + }, { file: 'front-end-block-theme.performance-results.json', metricsPrefix: 'block-theme-', diff --git a/changelog.txt b/changelog.txt index eb2cc01a796f7..4789d83da9768 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,225 @@ == Changelog == += 17.0.0-rc.1 = + + + +## Changelog + +### Features + +#### Data Views +- Add: Ability to persist dataviews on the database. ([55465](https://github.com/WordPress/gutenberg/pull/55465)) +- DataViews: Extract `search` from filters. ([55722](https://github.com/WordPress/gutenberg/pull/55722)) +- DataViews: Limit users to those who have published pages. ([55455](https://github.com/WordPress/gutenberg/pull/55455)) +- Dataviews: List all pages, not only those with publish and draft statuses. ([55476](https://github.com/WordPress/gutenberg/pull/55476)) + +#### Patterns +- Revert "Patterns: Fix capabilities settings for pattern categories. ([55532](https://github.com/WordPress/gutenberg/pull/55532)) + + +### Enhancements + +- Add blog icon to the blog home page in LinkControl search results. ([55610](https://github.com/WordPress/gutenberg/pull/55610)) +- Add: Default readonly views to dataviews. ([55740](https://github.com/WordPress/gutenberg/pull/55740)) +- Add: Possibility to tree shake DataViewsProvider. ([55641](https://github.com/WordPress/gutenberg/pull/55641)) +- Block Theme Preview: Display loading state when activating. ([55658](https://github.com/WordPress/gutenberg/pull/55658)) +- Improve toolbar button focus visual. ([55523](https://github.com/WordPress/gutenberg/pull/55523)) + +#### Components +- Add new ariakit-based `DropdownMenu` component. ([54939](https://github.com/WordPress/gutenberg/pull/54939)) +- Update InputControl and SelectControl default heights. ([55490](https://github.com/WordPress/gutenberg/pull/55490)) +- Update ariakit to version 0.3.5. ([55365](https://github.com/WordPress/gutenberg/pull/55365)) + +#### Block Library +- Allow using groups and columns inside the experimental form block. ([55758](https://github.com/WordPress/gutenberg/pull/55758)) + +#### Block Editor +- Adjust the padding of the category item to make it visually balanced. ([55598](https://github.com/WordPress/gutenberg/pull/55598)) + +#### Data Views +- [Data views]: Update icons and design tweaks. ([55391](https://github.com/WordPress/gutenberg/pull/55391)) + +#### Patterns +- Suggest commands when editing pattern in site editor. ([55332](https://github.com/WordPress/gutenberg/pull/55332)) + + +### Bug Fixes + +- Block Editor: Avoid rendering empty Slot for block alignments. ([55689](https://github.com/WordPress/gutenberg/pull/55689)) +- Block Example: Avoid a crash when block is not registered. ([55686](https://github.com/WordPress/gutenberg/pull/55686)) +- Command Palette: Fix a crash when transform to a block without icon. ([55676](https://github.com/WordPress/gutenberg/pull/55676)) +- Ensure Term Description block is registered in core. ([55669](https://github.com/WordPress/gutenberg/pull/55669)) +- Fix autocomplete trigger character detection. ([55301](https://github.com/WordPress/gutenberg/pull/55301)) +- Fix typo in `FontCollection`. ([55439](https://github.com/WordPress/gutenberg/pull/55439)) +- Fix: Add __next40pxDefaultSize to author select in PostAuthor. ([55597](https://github.com/WordPress/gutenberg/pull/55597)) +- Fix: Add home icon to the front page in LinkControl results. ([55606](https://github.com/WordPress/gutenberg/pull/55606)) +- Fix: Don't register dataviews postype and taxonomy if experiment is disabled. ([55743](https://github.com/WordPress/gutenberg/pull/55743)) +- Fix: LinkControl on site editor does not show font page and post page indication. ([55694](https://github.com/WordPress/gutenberg/pull/55694)) +- Fix: LinkControl on widgets editor does not show font page and post page indication. ([55732](https://github.com/WordPress/gutenberg/pull/55732)) +- Fix: Post terms block: Missing options for prefix and suffix when the…. ([55726](https://github.com/WordPress/gutenberg/pull/55726)) +- Fix: Update page title when using enhanced pagination in query loop. ([55446](https://github.com/WordPress/gutenberg/pull/55446)) +- Use PostCSS + PostCSS plugins for style transformation. ([49521](https://github.com/WordPress/gutenberg/pull/49521)) + +#### Block Library +- Fix #55679 missing space in the string. ([55682](https://github.com/WordPress/gutenberg/pull/55682)) +- Image block: Wrap images with hrefs in an A tag. ([55470](https://github.com/WordPress/gutenberg/pull/55470)) +- Image: Improve focus management in lightbox. ([55428](https://github.com/WordPress/gutenberg/pull/55428)) +- Image: Update default fullscreen icon for lightbox trigger. ([55463](https://github.com/WordPress/gutenberg/pull/55463)) +- Navigation block: Remove browser default padding from the two submenu buttons. ([50943](https://github.com/WordPress/gutenberg/pull/50943)) +- Query Loop: Disallow "enhanced pagination" with core blocks that may contain third-party blocks. ([55539](https://github.com/WordPress/gutenberg/pull/55539)) +- Query block enhanced pagination: Detect inner plugin blocks during render. ([55714](https://github.com/WordPress/gutenberg/pull/55714)) +- Query: Require queryId for enhanced pagination to prevent PHP notices. ([55720](https://github.com/WordPress/gutenberg/pull/55720)) +- Update label for lightbox editor UI. ([55724](https://github.com/WordPress/gutenberg/pull/55724)) +- [Block] File: Fix embedded PDF files in Safari. ([55667](https://github.com/WordPress/gutenberg/pull/55667)) + +#### Patterns +- Fix bug with authors and contributors not seeing user pattern categories. ([55553](https://github.com/WordPress/gutenberg/pull/55553)) +- Fix capabilities settings for pattern categories. ([55379](https://github.com/WordPress/gutenberg/pull/55379)) +- Fix pattern category renaming causing potential duplicate categories. ([55607](https://github.com/WordPress/gutenberg/pull/55607)) + +#### Post Editor +- Edit Post: Fix revision button misalignment. ([55659](https://github.com/WordPress/gutenberg/pull/55659)) +- Preferences modal: Fix the position of sticky heading when blocks are hidden. ([55456](https://github.com/WordPress/gutenberg/pull/55456)) + +#### Site Editor +- Fix the 'Slug' display for draft pages. ([55774](https://github.com/WordPress/gutenberg/pull/55774)) + +#### Data Views +- DataViews: List all users in author filter. ([55723](https://github.com/WordPress/gutenberg/pull/55723)) + +#### Design Tools +- Fix: Remove default dimensions controls from core/avatar. ([55596](https://github.com/WordPress/gutenberg/pull/55596)) + +#### Collaborative Editing +- Remove dangling comma causing PHPUnit failures. ([55494](https://github.com/WordPress/gutenberg/pull/55494)) + +#### Interactivity API +- Fix server processing of an empty `data-wp-context` directive. ([55482](https://github.com/WordPress/gutenberg/pull/55482)) + +#### Layout +- Make layout support compatible with enhanced pagination. ([55416](https://github.com/WordPress/gutenberg/pull/55416)) + +#### Colors +- Make duotone support compatible with enhanced pagination. ([55415](https://github.com/WordPress/gutenberg/pull/55415)) + +#### Global Styles +- Fix duotone not showing in site editor style block level styles. ([55361](https://github.com/WordPress/gutenberg/pull/55361)) + + +### Accessibility + +#### Block Library +- Navigation block: Change aria-haspopup to dialog. ([55466](https://github.com/WordPress/gutenberg/pull/55466)) + +#### Components +- Autocomplete: Fix Voiceover not announcing suggestions. ([54902](https://github.com/WordPress/gutenberg/pull/54902)) + +#### Interactivity API +- Query Loop: Update modal role and focus first element. ([54507](https://github.com/WordPress/gutenberg/pull/54507)) + + +### Performance + +- Add useSettings hook for reading multiple settings at once. ([55337](https://github.com/WordPress/gutenberg/pull/55337)) +- Block Editor: Optimize 'Layout' controls. ([55754](https://github.com/WordPress/gutenberg/pull/55754)) +- Block Editor: Optimize 'anchor' inspector controls. ([55721](https://github.com/WordPress/gutenberg/pull/55721)) +- Block Editor: Optimize 'duotone' controls. ([55753](https://github.com/WordPress/gutenberg/pull/55753)) +- Block Editor: Optimize alignment toolbar controls. ([55692](https://github.com/WordPress/gutenberg/pull/55692)) +- Start logging site editor metrics in codevitals. ([55759](https://github.com/WordPress/gutenberg/pull/55759)) +- useAvailableAlignments: Avoid useSelect subscription when alignments array is empty. ([55449](https://github.com/WordPress/gutenberg/pull/55449)) + + +### Experiments + +#### Data Views +- Trash Data View: Use trash icon. ([55771](https://github.com/WordPress/gutenberg/pull/55771)) +- [Data views]: Add quick actions. ([55488](https://github.com/WordPress/gutenberg/pull/55488)) + +#### Block Library +- Form Blocks: Capitalize title and end the description with a period. ([55728](https://github.com/WordPress/gutenberg/pull/55728)) + + +### Documentation + +- Adds a few grammar fixes to `block-registration` documentation. ([55663](https://github.com/WordPress/gutenberg/pull/55663)) +- Block JSON schema: Add heading/button key to color definition. ([55675](https://github.com/WordPress/gutenberg/pull/55675)) +- DataViews: Document `data` and `filters`. ([55504](https://github.com/WordPress/gutenberg/pull/55504)) +- Docs: Fix some typos. ([55654](https://github.com/WordPress/gutenberg/pull/55654)) +- Docs: Sidebar Block editor handbook - create-block before wp-scripts. ([55534](https://github.com/WordPress/gutenberg/pull/55534)) +- Fix: Typos inside /docs. ([55472](https://github.com/WordPress/gutenberg/pull/55472)) +- Scripts: Fix typo in readme. ([55531](https://github.com/WordPress/gutenberg/pull/55531)) +- Update links to point to getting started guides. ([55479](https://github.com/WordPress/gutenberg/pull/55479)) +- docs: Fix end-to-end test documentation formatting. ([55631](https://github.com/WordPress/gutenberg/pull/55631)) + + +### Code Quality + +- CS: Remove redundant ignore annotations. ([55615](https://github.com/WordPress/gutenberg/pull/55615)) +- Chore: Fix: Potential access to properties of undefined object. ([55697](https://github.com/WordPress/gutenberg/pull/55697)) +- Fix: Add missing defaultStatuses on PagePages. ([55761](https://github.com/WordPress/gutenberg/pull/55761)) +- Resizeable box (for resizing cover block) should use after block popover slot. ([55784](https://github.com/WordPress/gutenberg/pull/55784)) +- Update: Code quality: Use for each instead of map. ([55798](https://github.com/WordPress/gutenberg/pull/55798)) +- Update: Remove path awareness from data-views specific code. ([55695](https://github.com/WordPress/gutenberg/pull/55695)) +- Update: Remove useless self assignment. ([55696](https://github.com/WordPress/gutenberg/pull/55696)) +- chore: Fix: Remove unused file dataview context. ([55775](https://github.com/WordPress/gutenberg/pull/55775)) + +#### Data Views +- DataViews: Iterate on filter's API. ([55440](https://github.com/WordPress/gutenberg/pull/55440)) +- DataViews: Pass `search` filter as global, unattached from fields. ([55475](https://github.com/WordPress/gutenberg/pull/55475)) +- DataViews: Remove string from pages sidebar. ([55510](https://github.com/WordPress/gutenberg/pull/55510)) +- Rename `TextFilter` to `Search`. ([55731](https://github.com/WordPress/gutenberg/pull/55731)) + +#### Block Library +- Pattern: Unlock the private hook outside the component. ([55792](https://github.com/WordPress/gutenberg/pull/55792)) + +#### Patterns +- Change pattern category taxonomy public param to false. ([55748](https://github.com/WordPress/gutenberg/pull/55748)) + + +### Tools + +- Replace 'npx' with 'node' on test-playwright.js file'. ([55616](https://github.com/WordPress/gutenberg/pull/55616)) +- wp-env: Fix errors which prevent it from working without internet. ([53547](https://github.com/WordPress/gutenberg/pull/53547)) + +#### Testing +- E2E: Revert typing delay on the autocomplete mentions test. ([55132](https://github.com/WordPress/gutenberg/pull/55132)) +- Fix flaky 'Switch to Draft' action in Preview end-to-end tests. ([55772](https://github.com/WordPress/gutenberg/pull/55772)) +- Migrate 'Site Title block' end-to-end tests to Playwright. ([55704](https://github.com/WordPress/gutenberg/pull/55704)) +- Migrate 'block context' end-to-end tests to Playwright. ([55793](https://github.com/WordPress/gutenberg/pull/55793)) +- Performance tests: Fix canvas locator timeout. ([55441](https://github.com/WordPress/gutenberg/pull/55441)) +- Scripts: Fix default Playwright configuration. ([55453](https://github.com/WordPress/gutenberg/pull/55453)) + + +### Various + +- Form: Update copy. ([55468](https://github.com/WordPress/gutenberg/pull/55468)) +- [create-block] Add ABSPATH check. ([55533](https://github.com/WordPress/gutenberg/pull/55533)) + +#### HTML API +- Backport updates from Core during the 6.4 release cycle. ([55703](https://github.com/WordPress/gutenberg/pull/55703)) + +#### Collaborative Editing +- [Try] HTTP based PHP signaling server for colaborative editing. ([53922](https://github.com/WordPress/gutenberg/pull/53922)) + + +## First time contributors + +The following PRs were merged by first time contributors: + +- @garibiza: Fix #55679 missing space in the string. ([55682](https://github.com/WordPress/gutenberg/pull/55682)) +- @iamsadi22: Replace 'npx' with 'node' on test-playwright.js file'. ([55616](https://github.com/WordPress/gutenberg/pull/55616)) +- @parikshit-adhikari: Fix: Typos inside /docs. ([55472](https://github.com/WordPress/gutenberg/pull/55472)) +- @strarsis: Use PostCSS + PostCSS plugins for style transformation. ([49521](https://github.com/WordPress/gutenberg/pull/49521)) + + +## Contributors + +The following contributors merged PRs in this release: + +@aaronrobertshaw @ajlende @alexstine @aristath @artemiomorales @arthur791004 @aurooba @c4rl0sbr4v0 @carolinan @ciampo @DAreRodz @dcalhoun @derekblank @dmsnell @fluiddot @garibiza @geriux @glendaviesnz @iamsadi22 @jameskoster @jeryj @jorgefilipecosta @jsnajdr @juanmaguitar @kevin940726 @KevinBatdorf @luisherranz @MaggieCabrera @Mamaduka @noahtallen @ntsekouras @oandregal @parikshit-adhikari @peterwilsoncc @ramonjd @richtabor @SergeyBiryukov @SiobhyB @Soean @strarsis @swissspidy @t-hamano @talldan @WunderBart @youknowriad + + = 16.9.0 = ## Changelog diff --git a/docs/contributors/code/react-native/getting-started-react-native.md b/docs/contributors/code/react-native/getting-started-react-native.md index 55260a9c84154..7b4dcca98027d 100644 --- a/docs/contributors/code/react-native/getting-started-react-native.md +++ b/docs/contributors/code/react-native/getting-started-react-native.md @@ -139,7 +139,7 @@ Then, open `chrome://inspect` in Chrome to attach the debugger (look into the "R ## Writing and Running Unit Tests -This project is set up to use [jest](https://facebook.github.io/jest/) for tests. You can configure whatever testing strategy you like, but jest works out of the box. Create test files in directories called `__tests__` or with the `.test.js` extension to have the files loaded by jest. See an example test [here](https://github.com/WordPress/gutenberg/blob/HEAD/packages/react-native-editor/src/test/api-fetch-setup.test.js). The [jest documentation](https://facebook.github.io/jest/docs/en/getting-started.html) is also a wonderful resource, as is the [React Native testing tutorial](https://facebook.github.io/jest/docs/en/tutorial-react-native.html). +This project is set up to use [jest](https://jestjs.io/) for tests. You can configure whatever testing strategy you like, but jest works out of the box. Create test files in directories called `__tests__` or with the `.test.js` extension to have the files loaded by jest. See an example test [here](https://github.com/WordPress/gutenberg/blob/HEAD/packages/react-native-editor/src/test/api-fetch-setup.test.js). The [jest documentation](https://jestjs.io/docs/getting-started) is also a wonderful resource, as is the [React Native testing tutorial](https://jestjs.io/docs/tutorial-react-native). ## End-to-End Tests diff --git a/docs/contributors/documentation/README.md b/docs/contributors/documentation/README.md index a862592ab318a..397e7ad1e140c 100644 --- a/docs/contributors/documentation/README.md +++ b/docs/contributors/documentation/README.md @@ -25,14 +25,14 @@ The block editor handbook is a mix of markdown files in the `/docs/` directory o An automated job publishes the docs every 15 minutes to the [block editor handbook site](https://developer.wordpress.org/block-editor/). -See [the Git Workflow](/docs/contributors/code/git-workflow.md) documentation for how to use git to deploy changes using pull requests. Additionally, see the [video walk-through](https://wordpress.tv/2020/09/02/marcus-kazmierczak-contribute-developer-documentation-to-gutenberg/) and the accompanying [slides for contributing documentation to Gutenberg](https://mkaz.blog/wordpress/contribute-documentation-to-gutenberg/). +See [the Git Workflow](/docs/contributors/code/git-workflow.md) documentation for how to use git to deploy changes using pull requests. Additionally, see the [video walk-through](https://wordpress.tv/2020/09/02/marcus-kazmierczak-contribute-developer-documentation-to-gutenberg/) and the accompanying [slides for contributing documentation to Gutenberg](https://mkaz.blog/wordpress/contribute-developer-documentation-to-gutenberg/). ### Handbook structure The handbook is organized into four sections based on the functional types of documents. [The Documentation System](https://documentation.divio.com/) does a great job explaining the needs and functions of each type, but in short they are: - **Getting started tutorials** - full lessons that take learners step by step to complete an objective, for example the [create a block tutorial](/docs/getting-started/create-block/README.md). -- **How to guides** - short lessons specific to completing a small specific task, for example [how to add a button to the block toolbar](/docshow-to-guides/format-api/README.md). +- **How to guides** - short lessons specific to completing a small specific task, for example [how to add a button to the block toolbar](/docs/how-to-guides/format-api.md). - **Reference guides** - API documentation, purely functional descriptions, - **Explanations** - longer documentation focused on learning, not a specific task. diff --git a/docs/reference-guides/block-api/block-attributes.md b/docs/reference-guides/block-api/block-attributes.md index 0fbbeeb13680e..765d69584a669 100644 --- a/docs/reference-guides/block-api/block-attributes.md +++ b/docs/reference-guides/block-api/block-attributes.md @@ -357,7 +357,7 @@ Attribute available in the block: ### Meta source (deprecated)
-Although attributes may be obtained from a post's meta, meta attribute sources are considered deprecated; EntityProvider and related hook APIs should be used instead, as shown in the Create Meta Block how-to. +Although attributes may be obtained from a post's meta, meta attribute sources are considered deprecated; EntityProvider and related hook APIs should be used instead, as shown in the Create Meta Block how-to.
Attributes may be obtained from a post's meta rather than from the block's representation in saved post content. For this, an attribute is required to specify its corresponding meta key under the `meta` key. diff --git a/docs/reference-guides/core-blocks.md b/docs/reference-guides/core-blocks.md index 257efa62e15b6..e65853a6fbdf7 100644 --- a/docs/reference-guides/core-blocks.md +++ b/docs/reference-guides/core-blocks.md @@ -287,14 +287,13 @@ A form. ([Source](https://github.com/WordPress/gutenberg/tree/trunk/packages/blo - **Supports:** anchor, color (background, gradients, link, text), spacing (margin, padding), typography (fontSize, lineHeight), ~~className~~ - **Attributes:** action, email, method, submissionMethod -## Input field +## Input Field The basic building block for forms. ([Source](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-library/src/form-input)) - **Name:** core/form-input - **Experimental:** true - **Category:** common -- **Parent:** core/form - **Supports:** anchor, spacing (margin), ~~reusable~~ - **Attributes:** inlineLabel, label, name, placeholder, required, type, value, visibilityPermissions @@ -305,18 +304,16 @@ Provide a notification message after the form has been submitted. ([Source](http - **Name:** core/form-submission-notification - **Experimental:** true - **Category:** common -- **Parent:** core/form - **Supports:** - **Attributes:** type -## Form submit button +## Form Submit Button A submission button for forms. ([Source](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-library/src/form-submit-button)) - **Name:** core/form-submit-button - **Experimental:** true - **Category:** common -- **Parent:** core/form - **Supports:** - **Attributes:** diff --git a/docs/reference-guides/data/data-core-block-editor.md b/docs/reference-guides/data/data-core-block-editor.md index 7b0bd386daaf4..38a93552bcbef 100644 --- a/docs/reference-guides/data/data-core-block-editor.md +++ b/docs/reference-guides/data/data-core-block-editor.md @@ -588,6 +588,18 @@ _Properties_ - _isDisabled_ `boolean`: Whether or not the user should be prevented from inserting this item. - _frecency_ `number`: Heuristic that combines frequency and recency. +### getLastFocus + +Returns the element of the last element that had focus when focus left the editor canvas. + +_Parameters_ + +- _state_ `Object`: Block editor state. + +_Returns_ + +- `Object`: Element. + ### getLastMultiSelectedBlockClientId Returns the client ID of the last block in the multi-selection set, or null if there is no multi-selection. @@ -1651,6 +1663,18 @@ _Parameters_ - _clientId_ `string`: The block's clientId. - _hasControlledInnerBlocks_ `boolean`: True if the block's inner blocks are controlled. +### setLastFocus + +Action that sets the element that had focus when focus leaves the editor canvas. + +_Parameters_ + +- _lastFocus_ `Object`: The last focused element. + +_Returns_ + +- `Object`: Action object. + ### setNavigationMode Action that enables or disables the navigation mode. diff --git a/docs/reference-guides/filters/block-filters.md b/docs/reference-guides/filters/block-filters.md index 8970b9202b936..912403c483894 100644 --- a/docs/reference-guides/filters/block-filters.md +++ b/docs/reference-guides/filters/block-filters.md @@ -187,7 +187,7 @@ const { createHigherOrderComponent } = wp.compose; const { InspectorControls } = wp.blockEditor; const { PanelBody } = wp.components; -const withInspectorControls = createHigherOrderComponent( ( BlockEdit ) => { +const withMyPluginControls = createHigherOrderComponent( ( BlockEdit ) => { return ( props ) => { return ( <> @@ -198,12 +198,12 @@ const withInspectorControls = createHigherOrderComponent( ( BlockEdit ) => { ); }; -}, 'withInspectorControl' ); +}, 'withMyPluginControls' ); wp.hooks.addFilter( 'editor.BlockEdit', 'my-plugin/with-inspector-controls', - withInspectorControls + withMyPluginControls ); ``` @@ -212,7 +212,7 @@ wp.hooks.addFilter( ```js var el = React.createElement; -var withInspectorControls = wp.compose.createHigherOrderComponent( function ( +var withMyPluginControls = wp.compose.createHigherOrderComponent( function ( BlockEdit ) { return function ( props ) { @@ -227,12 +227,12 @@ var withInspectorControls = wp.compose.createHigherOrderComponent( function ( ) ); }; -}, 'withInspectorControls' ); +}, 'withMyPluginControls' ); wp.hooks.addFilter( 'editor.BlockEdit', 'my-plugin/with-inspector-controls', - withInspectorControls + withMyPluginControls ); ``` @@ -245,7 +245,7 @@ To mitigate this, consider whether any work you perform can be altered to run on For example, if you are adding components that only need to render when the block is _selected_, then you can use the block's "selected" state (`props.isSelected`) to conditionalize your rendering. ```js -const withInspectorControls = createHigherOrderComponent( ( BlockEdit ) => { +const withMyPluginControls = createHigherOrderComponent( ( BlockEdit ) => { return ( props ) => { return ( <> @@ -258,7 +258,7 @@ const withInspectorControls = createHigherOrderComponent( ( BlockEdit ) => { ); }; -}, 'withInspectorControl' ); +}, 'withMyPluginControls' ); ``` #### `editor.BlockListBlock` diff --git a/gutenberg.php b/gutenberg.php index 7934b13e4231a..13c82f3ea6455 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.2 * Requires PHP: 7.0 - * Version: 16.9.0 + * Version: 17.0.0-rc.1 * Author: Gutenberg Team * Text Domain: gutenberg * diff --git a/lib/compat/wordpress-6.3/html-api/class-gutenberg-html-tag-processor-6-3.php b/lib/compat/wordpress-6.3/html-api/class-gutenberg-html-tag-processor-6-3.php deleted file mode 100644 index 3b43d791692a9..0000000000000 --- a/lib/compat/wordpress-6.3/html-api/class-gutenberg-html-tag-processor-6-3.php +++ /dev/null @@ -1,2413 +0,0 @@ - "c" not " c". - * This would increase the size of the changes for some operations but leave more - * natural-looking output HTML. - * - Decode HTML character references within class names when matching. E.g. match having - * class `1<"2` needs to recognize `class="1<"2"`. Currently the Tag Processor - * will fail to find the right tag if the class name is encoded as such. - * - Properly decode HTML character references in `get_attribute()`. PHP's - * `html_entity_decode()` is wrong in a couple ways: it doesn't account for the - * no-ambiguous-ampersand rule, and it improperly handles the way semicolons may - * or may not terminate a character reference. - * - * @package WordPress - * @subpackage HTML-API - * @since 6.2.0 - */ - -/** - * Core class used to modify attributes in an HTML document for tags matching a query. - * - * ## Usage - * - * Use of this class requires three steps: - * - * 1. Create a new class instance with your input HTML document. - * 2. Find the tag(s) you are looking for. - * 3. Request changes to the attributes in those tag(s). - * - * Example: - * - * $tags = new WP_HTML_Tag_Processor( $html ); - * if ( $tags->next_tag( 'option' ) ) { - * $tags->set_attribute( 'selected', true ); - * } - * - * ### Finding tags - * - * The `next_tag()` function moves the internal cursor through - * your input HTML document until it finds a tag meeting any of - * the supplied restrictions in the optional query argument. If - * no argument is provided then it will find the next HTML tag, - * regardless of what kind it is. - * - * If you want to _find whatever the next tag is_: - * - * $tags->next_tag(); - * - * | Goal | Query | - * |-----------------------------------------------------------|---------------------------------------------------------------------------------| - * | Find any tag. | `$tags->next_tag();` | - * | Find next image tag. | `$tags->next_tag( array( 'tag_name' => 'img' ) );` | - * | Find next image tag (without passing the array). | `$tags->next_tag( 'img' );` | - * | Find next tag containing the `fullwidth` CSS class. | `$tags->next_tag( array( 'class_name' => 'fullwidth' ) );` | - * | Find next image tag containing the `fullwidth` CSS class. | `$tags->next_tag( array( 'tag_name' => 'img', 'class_name' => 'fullwidth' ) );` | - * - * If a tag was found meeting your criteria then `next_tag()` - * will return `true` and you can proceed to modify it. If it - * returns `false`, however, it failed to find the tag and - * moved the cursor to the end of the file. - * - * Once the cursor reaches the end of the file the processor - * is done and if you want to reach an earlier tag you will - * need to recreate the processor and start over, as it's - * unable to back up or move in reverse. - * - * See the section on bookmarks for an exception to this - * no-backing-up rule. - * - * #### Custom queries - * - * Sometimes it's necessary to further inspect an HTML tag than - * the query syntax here permits. In these cases one may further - * inspect the search results using the read-only functions - * provided by the processor or external state or variables. - * - * Example: - * - * // Paint up to the first five DIV or SPAN tags marked with the "jazzy" style. - * $remaining_count = 5; - * while ( $remaining_count > 0 && $tags->next_tag() ) { - * if ( - * ( 'DIV' === $tags->get_tag() || 'SPAN' === $tags->get_tag() ) && - * 'jazzy' === $tags->get_attribute( 'data-style' ) - * ) { - * $tags->add_class( 'theme-style-everest-jazz' ); - * $remaining_count--; - * } - * } - * - * `get_attribute()` will return `null` if the attribute wasn't present - * on the tag when it was called. It may return `""` (the empty string) - * in cases where the attribute was present but its value was empty. - * For boolean attributes, those whose name is present but no value is - * given, it will return `true` (the only way to set `false` for an - * attribute is to remove it). - * - * ### Modifying HTML attributes for a found tag - * - * Once you've found the start of an opening tag you can modify - * any number of the attributes on that tag. You can set a new - * value for an attribute, remove the entire attribute, or do - * nothing and move on to the next opening tag. - * - * Example: - * - * if ( $tags->next_tag( array( 'class' => 'wp-group-block' ) ) ) { - * $tags->set_attribute( 'title', 'This groups the contained content.' ); - * $tags->remove_attribute( 'data-test-id' ); - * } - * - * If `set_attribute()` is called for an existing attribute it will - * overwrite the existing value. Similarly, calling `remove_attribute()` - * for a non-existing attribute has no effect on the document. Both - * of these methods are safe to call without knowing if a given attribute - * exists beforehand. - * - * ### Modifying CSS classes for a found tag - * - * The tag processor treats the `class` attribute as a special case. - * Because it's a common operation to add or remove CSS classes, this - * interface adds helper methods to make that easier. - * - * As with attribute values, adding or removing CSS classes is a safe - * operation that doesn't require checking if the attribute or class - * exists before making changes. If removing the only class then the - * entire `class` attribute will be removed. - * - * Example: - * - * // from `Yippee!` - * // to `Yippee!` - * $tags->add_class( 'is-active' ); - * - * // from `Yippee!` - * // to `Yippee!` - * $tags->add_class( 'is-active' ); - * - * // from `Yippee!` - * // to `Yippee!` - * $tags->add_class( 'is-active' ); - * - * // from `` - * // to ` - * $tags->remove_class( 'rugby' ); - * - * // from `` - * // to ` - * $tags->remove_class( 'rugby' ); - * - * // from `` - * // to ` - * $tags->remove_class( 'rugby' ); - * - * When class changes are enqueued but a direct change to `class` is made via - * `set_attribute` then the changes to `set_attribute` (or `remove_attribute`) - * will take precedence over those made through `add_class` and `remove_class`. - * - * ### Bookmarks - * - * While scanning through the input HTMl document it's possible to set - * a named bookmark when a particular tag is found. Later on, after - * continuing to scan other tags, it's possible to `seek` to one of - * the set bookmarks and then proceed again from that point forward. - * - * Because bookmarks create processing overhead one should avoid - * creating too many of them. As a rule, create only bookmarks - * of known string literal names; avoid creating "mark_{$index}" - * and so on. It's fine from a performance standpoint to create a - * bookmark and update it frequently, such as within a loop. - * - * $total_todos = 0; - * while ( $p->next_tag( array( 'tag_name' => 'UL', 'class_name' => 'todo' ) ) ) { - * $p->set_bookmark( 'list-start' ); - * while ( $p->next_tag( array( 'tag_closers' => 'visit' ) ) ) { - * if ( 'UL' === $p->get_tag() && $p->is_tag_closer() ) { - * $p->set_bookmark( 'list-end' ); - * $p->seek( 'list-start' ); - * $p->set_attribute( 'data-contained-todos', (string) $total_todos ); - * $total_todos = 0; - * $p->seek( 'list-end' ); - * break; - * } - * - * if ( 'LI' === $p->get_tag() && ! $p->is_tag_closer() ) { - * $total_todos++; - * } - * } - * } - * - * ## Design and limitations - * - * The Tag Processor is designed to linearly scan HTML documents and tokenize - * HTML tags and their attributes. It's designed to do this as efficiently as - * possible without compromising parsing integrity. Therefore it will be - * slower than some methods of modifying HTML, such as those incorporating - * over-simplified PCRE patterns, but will not introduce the defects and - * failures that those methods bring in, which lead to broken page renders - * and often to security vulnerabilities. On the other hand, it will be faster - * than full-blown HTML parsers such as DOMDocument and use considerably - * less memory. It requires a negligible memory overhead, enough to consider - * it a zero-overhead system. - * - * The performance characteristics are maintained by avoiding tree construction - * and semantic cleanups which are specified in HTML5. Because of this, for - * example, it's not possible for the Tag Processor to associate any given - * opening tag with its corresponding closing tag, or to return the inner markup - * inside an element. Systems may be built on top of the Tag Processor to do - * this, but the Tag Processor is and should be constrained so it can remain an - * efficient, low-level, and reliable HTML scanner. - * - * The Tag Processor's design incorporates a "garbage-in-garbage-out" philosophy. - * HTML5 specifies that certain invalid content be transformed into different forms - * for display, such as removing null bytes from an input document and replacing - * invalid characters with the Unicode replacement character `U+FFFD` (visually "�"). - * Where errors or transformations exist within the HTML5 specification, the Tag Processor - * leaves those invalid inputs untouched, passing them through to the final browser - * to handle. While this implies that certain operations will be non-spec-compliant, - * such as reading the value of an attribute with invalid content, it also preserves a - * simplicity and efficiency for handling those error cases. - * - * Most operations within the Tag Processor are designed to minimize the difference - * between an input and output document for any given change. For example, the - * `add_class` and `remove_class` methods preserve whitespace and the class ordering - * within the `class` attribute; and when encountering tags with duplicated attributes, - * the Tag Processor will leave those invalid duplicate attributes where they are but - * update the proper attribute which the browser will read for parsing its value. An - * exception to this rule is that all attribute updates store their values as - * double-quoted strings, meaning that attributes on input with single-quoted or - * unquoted values will appear in the output with double-quotes. - * - * @since 6.2.0 - * @since 6.2.1 Fix: Support for various invalid comments; attribute updates are case-insensitive. - * @since 6.3.2 Fix: Skip HTML-like content inside rawtext elements such as STYLE. - */ -class Gutenberg_HTML_Tag_Processor_6_3 { - /** - * The maximum number of bookmarks allowed to exist at - * any given time. - * - * @since 6.2.0 - * @var int - * - * @see WP_HTML_Tag_Processor::set_bookmark() - */ - const MAX_BOOKMARKS = 10; - - /** - * Maximum number of times seek() can be called. - * Prevents accidental infinite loops. - * - * @since 6.2.0 - * @var int - * - * @see WP_HTML_Tag_Processor::seek() - */ - const MAX_SEEK_OPS = 1000; - - /** - * The HTML document to parse. - * - * @since 6.2.0 - * @var string - */ - protected $html; - - /** - * The last query passed to next_tag(). - * - * @since 6.2.0 - * @var array|null - */ - private $last_query; - - /** - * The tag name this processor currently scans for. - * - * @since 6.2.0 - * @var string|null - */ - private $sought_tag_name; - - /** - * The CSS class name this processor currently scans for. - * - * @since 6.2.0 - * @var string|null - */ - private $sought_class_name; - - /** - * The match offset this processor currently scans for. - * - * @since 6.2.0 - * @var int|null - */ - private $sought_match_offset; - - /** - * Whether to visit tag closers, e.g. , when walking an input document. - * - * @since 6.2.0 - * @var bool - */ - private $stop_on_tag_closers; - - /** - * How many bytes from the original HTML document have been read and parsed. - * - * This value points to the latest byte offset in the input document which - * has been already parsed. It is the internal cursor for the Tag Processor - * and updates while scanning through the HTML tokens. - * - * @since 6.2.0 - * @var int - */ - private $bytes_already_parsed = 0; - - /** - * Byte offset in input document where current tag name starts. - * - * Example: - * - *
... - * 01234 - * - tag name starts at 1 - * - * @since 6.2.0 - * @var int|null - */ - private $tag_name_starts_at; - - /** - * Byte length of current tag name. - * - * Example: - * - *
... - * 01234 - * --- tag name length is 3 - * - * @since 6.2.0 - * @var int|null - */ - private $tag_name_length; - - /** - * Byte offset in input document where current tag token ends. - * - * Example: - * - *
... - * 0 1 | - * 01234567890123456 - * --- tag name ends at 14 - * - * @since 6.2.0 - * @var int|null - */ - private $tag_ends_at; - - /** - * Whether the current tag is an opening tag, e.g.
, or a closing tag, e.g.
. - * - * @var bool - */ - private $is_closing_tag; - - /** - * Lazily-built index of attributes found within an HTML tag, keyed by the attribute name. - * - * Example: - * - * // Supposing the parser is working through this content - * // and stops after recognizing the `id` attribute. - * //
- * // ^ parsing will continue from this point. - * $this->attributes = array( - * 'id' => new WP_HTML_Attribute_Match( 'id', null, 6, 17 ) - * ); - * - * // When picking up parsing again, or when asking to find the - * // `class` attribute we will continue and add to this array. - * $this->attributes = array( - * 'id' => new WP_HTML_Attribute_Match( 'id', null, 6, 17 ), - * 'class' => new WP_HTML_Attribute_Match( 'class', 'outline', 18, 32 ) - * ); - * - * // Note that only the `class` attribute value is stored in the index. - * // That's because it is the only value used by this class at the moment. - * - * @since 6.2.0 - * @var WP_HTML_Attribute_Token[] - */ - private $attributes = array(); - - /** - * Tracks spans of duplicate attributes on a given tag, used for removing - * all copies of an attribute when calling `remove_attribute()`. - * - * @since 6.3.2 - * - * @var (WP_HTML_Span[])[]|null - */ - private $duplicate_attributes = null; - - /** - * Which class names to add or remove from a tag. - * - * These are tracked separately from attribute updates because they are - * semantically distinct, whereas this interface exists for the common - * case of adding and removing class names while other attributes are - * generally modified as with DOM `setAttribute` calls. - * - * When modifying an HTML document these will eventually be collapsed - * into a single `set_attribute( 'class', $changes )` call. - * - * Example: - * - * // Add the `wp-block-group` class, remove the `wp-group` class. - * $classname_updates = array( - * // Indexed by a comparable class name. - * 'wp-block-group' => WP_HTML_Tag_Processor::ADD_CLASS, - * 'wp-group' => WP_HTML_Tag_Processor::REMOVE_CLASS - * ); - * - * @since 6.2.0 - * @var bool[] - */ - private $classname_updates = array(); - - /** - * Tracks a semantic location in the original HTML which - * shifts with updates as they are applied to the document. - * - * @since 6.2.0 - * @var WP_HTML_Span[] - */ - protected $bookmarks = array(); - - const ADD_CLASS = true; - const REMOVE_CLASS = false; - const SKIP_CLASS = null; - - /** - * Lexical replacements to apply to input HTML document. - * - * "Lexical" in this class refers to the part of this class which - * operates on pure text _as text_ and not as HTML. There's a line - * between the public interface, with HTML-semantic methods like - * `set_attribute` and `add_class`, and an internal state that tracks - * text offsets in the input document. - * - * When higher-level HTML methods are called, those have to transform their - * operations (such as setting an attribute's value) into text diffing - * operations (such as replacing the sub-string from indices A to B with - * some given new string). These text-diffing operations are the lexical - * updates. - * - * As new higher-level methods are added they need to collapse their - * operations into these lower-level lexical updates since that's the - * Tag Processor's internal language of change. Any code which creates - * these lexical updates must ensure that they do not cross HTML syntax - * boundaries, however, so these should never be exposed outside of this - * class or any classes which intentionally expand its functionality. - * - * These are enqueued while editing the document instead of being immediately - * applied to avoid processing overhead, string allocations, and string - * copies when applying many updates to a single document. - * - * Example: - * - * // Replace an attribute stored with a new value, indices - * // sourced from the lazily-parsed HTML recognizer. - * $start = $attributes['src']->start; - * $end = $attributes['src']->end; - * $modifications[] = new WP_HTML_Text_Replacement( $start, $end, $new_value ); - * - * // Correspondingly, something like this will appear in this array. - * $lexical_updates = array( - * WP_HTML_Text_Replacement( 14, 28, 'https://my-site.my-domain/wp-content/uploads/2014/08/kittens.jpg' ) - * ); - * - * @since 6.2.0 - * @var WP_HTML_Text_Replacement[] - */ - protected $lexical_updates = array(); - - /** - * Tracks and limits `seek()` calls to prevent accidental infinite loops. - * - * @since 6.2.0 - * @var int - * - * @see WP_HTML_Tag_Processor::seek() - */ - protected $seek_count = 0; - - /** - * Constructor. - * - * @since 6.2.0 - * - * @param string $html HTML to process. - */ - public function __construct( $html ) { - $this->html = $html; - } - - /** - * Finds the next tag matching the $query. - * - * @since 6.2.0 - * - * @param array|string|null $query { - * Optional. Which tag name to find, having which class, etc. Default is to find any tag. - * - * @type string|null $tag_name Which tag to find, or `null` for "any tag." - * @type int|null $match_offset Find the Nth tag matching all search criteria. - * 1 for "first" tag, 3 for "third," etc. - * Defaults to first tag. - * @type string|null $class_name Tag must contain this whole class name to match. - * @type string|null $tag_closers "visit" or "skip": whether to stop on tag closers, e.g.
. - * } - * @return boolean Whether a tag was matched. - */ - public function next_tag( $query = null ) { - $this->parse_query( $query ); - $already_found = 0; - - do { - if ( $this->bytes_already_parsed >= strlen( $this->html ) ) { - return false; - } - - // Find the next tag if it exists. - if ( false === $this->parse_next_tag() ) { - $this->bytes_already_parsed = strlen( $this->html ); - - return false; - } - - // Parse all of its attributes. - while ( $this->parse_next_attribute() ) { - continue; - } - - // Ensure that the tag closes before the end of the document. - if ( $this->bytes_already_parsed >= strlen( $this->html ) ) { - return false; - } - - $tag_ends_at = strpos( $this->html, '>', $this->bytes_already_parsed ); - if ( false === $tag_ends_at ) { - return false; - } - $this->tag_ends_at = $tag_ends_at; - $this->bytes_already_parsed = $tag_ends_at; - - // Finally, check if the parsed tag and its attributes match the search query. - if ( $this->matches() ) { - ++$already_found; - } - - /* - * For non-DATA sections which might contain text that looks like HTML tags but - * isn't, scan with the appropriate alternative mode. Looking at the first letter - * of the tag name as a pre-check avoids a string allocation when it's not needed. - */ - $t = $this->html[ $this->tag_name_starts_at ]; - if ( - ! $this->is_closing_tag && - ( - 'i' === $t || 'I' === $t || - 'n' === $t || 'N' === $t || - 's' === $t || 'S' === $t || - 't' === $t || 'T' === $t - ) ) { - $tag_name = $this->get_tag(); - - if ( 'SCRIPT' === $tag_name && ! $this->skip_script_data() ) { - $this->bytes_already_parsed = strlen( $this->html ); - return false; - } elseif ( - ( 'TEXTAREA' === $tag_name || 'TITLE' === $tag_name ) && - ! $this->skip_rcdata( $tag_name ) - ) { - $this->bytes_already_parsed = strlen( $this->html ); - return false; - } elseif ( - ( - 'IFRAME' === $tag_name || - 'NOEMBED' === $tag_name || - 'NOFRAMES' === $tag_name || - 'NOSCRIPT' === $tag_name || - 'STYLE' === $tag_name - ) && - ! $this->skip_rawtext( $tag_name ) - ) { - /* - * "XMP" should be here too but its rules are more complicated and require the - * complexity of the HTML Processor (it needs to close out any open P element, - * meaning it can't be skipped here or else the HTML Processor will lose its - * place). For now, it can be ignored as it's a rare HTML tag in practice and - * any normative HTML should be using PRE instead. - */ - $this->bytes_already_parsed = strlen( $this->html ); - return false; - } - } - } while ( $already_found < $this->sought_match_offset ); - - return true; - } - - - /** - * Sets a bookmark in the HTML document. - * - * Bookmarks represent specific places or tokens in the HTML - * document, such as a tag opener or closer. When applying - * edits to a document, such as setting an attribute, the - * text offsets of that token may shift; the bookmark is - * kept updated with those shifts and remains stable unless - * the entire span of text in which the token sits is removed. - * - * Release bookmarks when they are no longer needed. - * - * Example: - * - *

Surprising fact you may not know!

- * ^ ^ - * \-|-- this `H2` opener bookmark tracks the token - * - *

Surprising fact you may no… - * ^ ^ - * \-|-- it shifts with edits - * - * Bookmarks provide the ability to seek to a previously-scanned - * place in the HTML document. This avoids the need to re-scan - * the entire document. - * - * Example: - * - *
  • One
  • Two
  • Three
- * ^^^^ - * want to note this last item - * - * $p = new WP_HTML_Tag_Processor( $html ); - * $in_list = false; - * while ( $p->next_tag( array( 'tag_closers' => $in_list ? 'visit' : 'skip' ) ) ) { - * if ( 'UL' === $p->get_tag() ) { - * if ( $p->is_tag_closer() ) { - * $in_list = false; - * $p->set_bookmark( 'resume' ); - * if ( $p->seek( 'last-li' ) ) { - * $p->add_class( 'last-li' ); - * } - * $p->seek( 'resume' ); - * $p->release_bookmark( 'last-li' ); - * $p->release_bookmark( 'resume' ); - * } else { - * $in_list = true; - * } - * } - * - * if ( 'LI' === $p->get_tag() ) { - * $p->set_bookmark( 'last-li' ); - * } - * } - * - * Bookmarks intentionally hide the internal string offsets - * to which they refer. They are maintained internally as - * updates are applied to the HTML document and therefore - * retain their "position" - the location to which they - * originally pointed. The inability to use bookmarks with - * functions like `substr` is therefore intentional to guard - * against accidentally breaking the HTML. - * - * Because bookmarks allocate memory and require processing - * for every applied update, they are limited and require - * a name. They should not be created with programmatically-made - * names, such as "li_{$index}" with some loop. As a general - * rule they should only be created with string-literal names - * like "start-of-section" or "last-paragraph". - * - * Bookmarks are a powerful tool to enable complicated behavior. - * Consider double-checking that you need this tool if you are - * reaching for it, as inappropriate use could lead to broken - * HTML structure or unwanted processing overhead. - * - * @since 6.2.0 - * - * @param string $name Identifies this particular bookmark. - * @return bool Whether the bookmark was successfully created. - */ - public function set_bookmark( $name ) { - if ( null === $this->tag_name_starts_at ) { - return false; - } - - if ( ! array_key_exists( $name, $this->bookmarks ) && count( $this->bookmarks ) >= self::MAX_BOOKMARKS ) { - _doing_it_wrong( - __METHOD__, - __( 'Too many bookmarks: cannot create any more.' ), - '6.2.0' - ); - return false; - } - - $this->bookmarks[ $name ] = new WP_HTML_Span( - $this->tag_name_starts_at - ( $this->is_closing_tag ? 2 : 1 ), - $this->tag_ends_at - ); - - return true; - } - - - /** - * Removes a bookmark that is no longer needed. - * - * Releasing a bookmark frees up the small - * performance overhead it requires. - * - * @param string $name Name of the bookmark to remove. - * @return bool Whether the bookmark already existed before removal. - */ - public function release_bookmark( $name ) { - if ( ! array_key_exists( $name, $this->bookmarks ) ) { - return false; - } - - unset( $this->bookmarks[ $name ] ); - - return true; - } - - /** - * Skips contents of generic rawtext elements. - * - * @since 6.3.2 - * - * @see https://html.spec.whatwg.org/#generic-raw-text-element-parsing-algorithm - * - * @param string $tag_name The uppercase tag name which will close the RAWTEXT region. - * @return bool Whether an end to the RAWTEXT region was found before the end of the document. - */ - private function skip_rawtext( $tag_name ) { - /* - * These two functions distinguish themselves on whether character references are - * decoded, and since functionality to read the inner markup isn't supported, it's - * not necessary to implement these two functions separately. - */ - return $this->skip_rcdata( $tag_name ); - } - - /** - * Skips contents of RCDATA elements, namely title and textarea tags. - * - * @since 6.2.0 - * - * @see https://html.spec.whatwg.org/multipage/parsing.html#rcdata-state - * - * @param string $tag_name The uppercase tag name which will close the RCDATA region. - * @return bool Whether an end to the RCDATA region was found before the end of the document. - */ - private function skip_rcdata( $tag_name ) { - $html = $this->html; - $doc_length = strlen( $html ); - $tag_length = strlen( $tag_name ); - - $at = $this->bytes_already_parsed; - - while ( false !== $at && $at < $doc_length ) { - $at = strpos( $this->html, '= $doc_length ) { - $this->bytes_already_parsed = $doc_length; - return false; - } - - $closer_potentially_starts_at = $at; - $at += 2; - - /* - * Find a case-insensitive match to the tag name. - * - * Because tag names are limited to US-ASCII there is no - * need to perform any kind of Unicode normalization when - * comparing; any character which could be impacted by such - * normalization could not be part of a tag name. - */ - for ( $i = 0; $i < $tag_length; $i++ ) { - $tag_char = $tag_name[ $i ]; - $html_char = $html[ $at + $i ]; - - if ( $html_char !== $tag_char && strtoupper( $html_char ) !== $tag_char ) { - $at += $i; - continue 2; - } - } - - $at += $tag_length; - $this->bytes_already_parsed = $at; - - /* - * Ensure that the tag name terminates to avoid matching on - * substrings of a longer tag name. For example, the sequence - * "' !== $c ) { - continue; - } - - while ( $this->parse_next_attribute() ) { - continue; - } - $at = $this->bytes_already_parsed; - if ( $at >= strlen( $this->html ) ) { - return false; - } - - if ( '>' === $html[ $at ] || '/' === $html[ $at ] ) { - $this->bytes_already_parsed = $closer_potentially_starts_at; - return true; - } - } - - return false; - } - - /** - * Skips contents of script tags. - * - * @since 6.2.0 - * - * @return bool Whether the script tag was closed before the end of the document. - */ - private function skip_script_data() { - $state = 'unescaped'; - $html = $this->html; - $doc_length = strlen( $html ); - $at = $this->bytes_already_parsed; - - while ( false !== $at && $at < $doc_length ) { - $at += strcspn( $html, '-<', $at ); - - /* - * For all script states a "-->" transitions - * back into the normal unescaped script mode, - * even if that's the current state. - */ - if ( - $at + 2 < $doc_length && - '-' === $html[ $at ] && - '-' === $html[ $at + 1 ] && - '>' === $html[ $at + 2 ] - ) { - $at += 3; - $state = 'unescaped'; - continue; - } - - // Everything of interest past here starts with "<". - if ( $at + 1 >= $doc_length || '<' !== $html[ $at++ ] ) { - continue; - } - - /* - * Unlike with "-->", the " - * https://html.spec.whatwg.org/multipage/parsing.html#tag-open-state - */ - if ( - strlen( $html ) > $at + 3 && - '-' === $html[ $at + 2 ] && - '-' === $html[ $at + 3 ] - ) { - $closer_at = $at + 4; - // If it's not possible to close the comment then there is nothing more to scan. - if ( strlen( $html ) <= $closer_at ) { - return false; - } - - // Abruptly-closed empty comments are a sequence of dashes followed by `>`. - $span_of_dashes = strspn( $html, '-', $closer_at ); - if ( '>' === $html[ $closer_at + $span_of_dashes ] ) { - $at = $closer_at + $span_of_dashes + 1; - continue; - } - - /* - * Comments may be closed by either a --> or an invalid --!>. - * The first occurrence closes the comment. - * - * See https://html.spec.whatwg.org/#parse-error-incorrectly-closed-comment - */ - $closer_at--; // Pre-increment inside condition below reduces risk of accidental infinite looping. - while ( ++$closer_at < strlen( $html ) ) { - $closer_at = strpos( $html, '--', $closer_at ); - if ( false === $closer_at ) { - return false; - } - - if ( $closer_at + 2 < strlen( $html ) && '>' === $html[ $closer_at + 2 ] ) { - $at = $closer_at + 3; - continue 2; - } - - if ( $closer_at + 3 < strlen( $html ) && '!' === $html[ $closer_at + 2 ] && '>' === $html[ $closer_at + 3 ] ) { - $at = $closer_at + 4; - continue 2; - } - } - } - - /* - * - * The CDATA is case-sensitive. - * https://html.spec.whatwg.org/multipage/parsing.html#tag-open-state - */ - if ( - strlen( $html ) > $at + 8 && - '[' === $html[ $at + 2 ] && - 'C' === $html[ $at + 3 ] && - 'D' === $html[ $at + 4 ] && - 'A' === $html[ $at + 5 ] && - 'T' === $html[ $at + 6 ] && - 'A' === $html[ $at + 7 ] && - '[' === $html[ $at + 8 ] - ) { - $closer_at = strpos( $html, ']]>', $at + 9 ); - if ( false === $closer_at ) { - return false; - } - - $at = $closer_at + 3; - continue; - } - - /* - * - * These are ASCII-case-insensitive. - * https://html.spec.whatwg.org/multipage/parsing.html#tag-open-state - */ - if ( - strlen( $html ) > $at + 8 && - ( 'D' === $html[ $at + 2 ] || 'd' === $html[ $at + 2 ] ) && - ( 'O' === $html[ $at + 3 ] || 'o' === $html[ $at + 3 ] ) && - ( 'C' === $html[ $at + 4 ] || 'c' === $html[ $at + 4 ] ) && - ( 'T' === $html[ $at + 5 ] || 't' === $html[ $at + 5 ] ) && - ( 'Y' === $html[ $at + 6 ] || 'y' === $html[ $at + 6 ] ) && - ( 'P' === $html[ $at + 7 ] || 'p' === $html[ $at + 7 ] ) && - ( 'E' === $html[ $at + 8 ] || 'e' === $html[ $at + 8 ] ) - ) { - $closer_at = strpos( $html, '>', $at + 9 ); - if ( false === $closer_at ) { - return false; - } - - $at = $closer_at + 1; - continue; - } - - /* - * Anything else here is an incorrectly-opened comment and transitions - * to the bogus comment state - skip to the nearest >. - */ - $at = strpos( $html, '>', $at + 1 ); - continue; - } - - /* - * is a missing end tag name, which is ignored. - * - * See https://html.spec.whatwg.org/#parse-error-missing-end-tag-name - */ - if ( '>' === $html[ $at + 1 ] ) { - $at++; - continue; - } - - /* - * - * See https://html.spec.whatwg.org/multipage/parsing.html#tag-open-state - */ - if ( '?' === $html[ $at + 1 ] ) { - $closer_at = strpos( $html, '>', $at + 2 ); - if ( false === $closer_at ) { - return false; - } - - $at = $closer_at + 1; - continue; - } - - /* - * If a non-alpha starts the tag name in a tag closer it's a comment. - * Find the first `>`, which closes the comment. - * - * See https://html.spec.whatwg.org/#parse-error-invalid-first-character-of-tag-name - */ - if ( $this->is_closing_tag ) { - $closer_at = strpos( $html, '>', $at + 3 ); - if ( false === $closer_at ) { - return false; - } - - $at = $closer_at + 1; - continue; - } - - ++$at; - } - - return false; - } - - /** - * Parses the next attribute. - * - * @since 6.2.0 - * - * @return bool Whether an attribute was found before the end of the document. - */ - private function parse_next_attribute() { - // Skip whitespace and slashes. - $this->bytes_already_parsed += strspn( $this->html, " \t\f\r\n/", $this->bytes_already_parsed ); - if ( $this->bytes_already_parsed >= strlen( $this->html ) ) { - return false; - } - - /* - * Treat the equal sign as a part of the attribute - * name if it is the first encountered byte. - * - * @see https://html.spec.whatwg.org/multipage/parsing.html#before-attribute-name-state - */ - $name_length = '=' === $this->html[ $this->bytes_already_parsed ] - ? 1 + strcspn( $this->html, "=/> \t\f\r\n", $this->bytes_already_parsed + 1 ) - : strcspn( $this->html, "=/> \t\f\r\n", $this->bytes_already_parsed ); - - // No attribute, just tag closer. - if ( 0 === $name_length || $this->bytes_already_parsed + $name_length >= strlen( $this->html ) ) { - return false; - } - - $attribute_start = $this->bytes_already_parsed; - $attribute_name = substr( $this->html, $attribute_start, $name_length ); - $this->bytes_already_parsed += $name_length; - if ( $this->bytes_already_parsed >= strlen( $this->html ) ) { - return false; - } - - $this->skip_whitespace(); - if ( $this->bytes_already_parsed >= strlen( $this->html ) ) { - return false; - } - - $has_value = '=' === $this->html[ $this->bytes_already_parsed ]; - if ( $has_value ) { - ++$this->bytes_already_parsed; - $this->skip_whitespace(); - if ( $this->bytes_already_parsed >= strlen( $this->html ) ) { - return false; - } - - switch ( $this->html[ $this->bytes_already_parsed ] ) { - case "'": - case '"': - $quote = $this->html[ $this->bytes_already_parsed ]; - $value_start = $this->bytes_already_parsed + 1; - $value_length = strcspn( $this->html, $quote, $value_start ); - $attribute_end = $value_start + $value_length + 1; - $this->bytes_already_parsed = $attribute_end; - break; - - default: - $value_start = $this->bytes_already_parsed; - $value_length = strcspn( $this->html, "> \t\f\r\n", $value_start ); - $attribute_end = $value_start + $value_length; - $this->bytes_already_parsed = $attribute_end; - } - } else { - $value_start = $this->bytes_already_parsed; - $value_length = 0; - $attribute_end = $attribute_start + $name_length; - } - - if ( $attribute_end >= strlen( $this->html ) ) { - return false; - } - - if ( $this->is_closing_tag ) { - return true; - } - - /* - * > There must never be two or more attributes on - * > the same start tag whose names are an ASCII - * > case-insensitive match for each other. - * - HTML 5 spec - * - * @see https://html.spec.whatwg.org/multipage/syntax.html#attributes-2:ascii-case-insensitive - */ - $comparable_name = strtolower( $attribute_name ); - - // If an attribute is listed many times, only use the first declaration and ignore the rest. - if ( ! array_key_exists( $comparable_name, $this->attributes ) ) { - $this->attributes[ $comparable_name ] = new WP_HTML_Attribute_Token( - $attribute_name, - $value_start, - $value_length, - $attribute_start, - $attribute_end, - ! $has_value - ); - - return true; - } - - /* - * Track the duplicate attributes so if we remove it, all disappear together. - * - * While `$this->duplicated_attributes` could always be stored as an `array()`, - * which would simplify the logic here, storing a `null` and only allocating - * an array when encountering duplicates avoids needless allocations in the - * normative case of parsing tags with no duplicate attributes. - */ - $duplicate_span = new WP_HTML_Span( $attribute_start, $attribute_end ); - if ( null === $this->duplicate_attributes ) { - $this->duplicate_attributes = array( $comparable_name => array( $duplicate_span ) ); - } elseif ( ! array_key_exists( $comparable_name, $this->duplicate_attributes ) ) { - $this->duplicate_attributes[ $comparable_name ] = array( $duplicate_span ); - } else { - $this->duplicate_attributes[ $comparable_name ][] = $duplicate_span; - } - - return true; - } - - /** - * Move the internal cursor past any immediate successive whitespace. - * - * @since 6.2.0 - */ - private function skip_whitespace() { - $this->bytes_already_parsed += strspn( $this->html, " \t\f\r\n", $this->bytes_already_parsed ); - } - - /** - * Applies attribute updates and cleans up once a tag is fully parsed. - * - * @since 6.2.0 - */ - private function after_tag() { - $this->get_updated_html(); - $this->tag_name_starts_at = null; - $this->tag_name_length = null; - $this->tag_ends_at = null; - $this->is_closing_tag = null; - $this->attributes = array(); - $this->duplicate_attributes = null; - } - - /** - * Converts class name updates into tag attributes updates - * (they are accumulated in different data formats for performance). - * - * @since 6.2.0 - * - * @see WP_HTML_Tag_Processor::$lexical_updates - * @see WP_HTML_Tag_Processor::$classname_updates - */ - private function class_name_updates_to_attributes_updates() { - if ( count( $this->classname_updates ) === 0 ) { - return; - } - - $existing_class = $this->get_enqueued_attribute_value( 'class' ); - if ( null === $existing_class || true === $existing_class ) { - $existing_class = ''; - } - - if ( false === $existing_class && isset( $this->attributes['class'] ) ) { - $existing_class = substr( - $this->html, - $this->attributes['class']->value_starts_at, - $this->attributes['class']->value_length - ); - } - - if ( false === $existing_class ) { - $existing_class = ''; - } - - /** - * Updated "class" attribute value. - * - * This is incrementally built while scanning through the existing class - * attribute, skipping removed classes on the way, and then appending - * added classes at the end. Only when finished processing will the - * value contain the final new value. - - * @var string $class - */ - $class = ''; - - /** - * Tracks the cursor position in the existing - * class attribute value while parsing. - * - * @var int $at - */ - $at = 0; - - /** - * Indicates if there's any need to modify the existing class attribute. - * - * If a call to `add_class()` and `remove_class()` wouldn't impact - * the `class` attribute value then there's no need to rebuild it. - * For example, when adding a class that's already present or - * removing one that isn't. - * - * This flag enables a performance optimization when none of the enqueued - * class updates would impact the `class` attribute; namely, that the - * processor can continue without modifying the input document, as if - * none of the `add_class()` or `remove_class()` calls had been made. - * - * This flag is set upon the first change that requires a string update. - * - * @var bool $modified - */ - $modified = false; - - // Remove unwanted classes by only copying the new ones. - $existing_class_length = strlen( $existing_class ); - while ( $at < $existing_class_length ) { - // Skip to the first non-whitespace character. - $ws_at = $at; - $ws_length = strspn( $existing_class, " \t\f\r\n", $ws_at ); - $at += $ws_length; - - // Capture the class name – it's everything until the next whitespace. - $name_length = strcspn( $existing_class, " \t\f\r\n", $at ); - if ( 0 === $name_length ) { - // If no more class names are found then that's the end. - break; - } - - $name = substr( $existing_class, $at, $name_length ); - $at += $name_length; - - // If this class is marked for removal, start processing the next one. - $remove_class = ( - isset( $this->classname_updates[ $name ] ) && - self::REMOVE_CLASS === $this->classname_updates[ $name ] - ); - - // If a class has already been seen then skip it; it should not be added twice. - if ( ! $remove_class ) { - $this->classname_updates[ $name ] = self::SKIP_CLASS; - } - - if ( $remove_class ) { - $modified = true; - continue; - } - - /* - * Otherwise, append it to the new "class" attribute value. - * - * There are options for handling whitespace between tags. - * Preserving the existing whitespace produces fewer changes - * to the HTML content and should clarify the before/after - * content when debugging the modified output. - * - * This approach contrasts normalizing the inter-class - * whitespace to a single space, which might appear cleaner - * in the output HTML but produce a noisier change. - */ - $class .= substr( $existing_class, $ws_at, $ws_length ); - $class .= $name; - } - - // Add new classes by appending those which haven't already been seen. - foreach ( $this->classname_updates as $name => $operation ) { - if ( self::ADD_CLASS === $operation ) { - $modified = true; - - $class .= strlen( $class ) > 0 ? ' ' : ''; - $class .= $name; - } - } - - $this->classname_updates = array(); - if ( ! $modified ) { - return; - } - - if ( strlen( $class ) > 0 ) { - $this->set_attribute( 'class', $class ); - } else { - $this->remove_attribute( 'class' ); - } - } - - /** - * Applies attribute updates to HTML document. - * - * @since 6.2.0 - * @since 6.2.1 Accumulates shift for internal cursor and passed pointer. - * @since 6.3.0 Invalidate any bookmarks whose targets are overwritten. - * - * @param int $shift_this_point Accumulate and return shift for this position. - * @return int How many bytes the given pointer moved in response to the updates. - */ - private function apply_attributes_updates( $shift_this_point = 0 ) { - if ( ! count( $this->lexical_updates ) ) { - return 0; - } - - $accumulated_shift_for_given_point = 0; - - /* - * Attribute updates can be enqueued in any order but updates - * to the document must occur in lexical order; that is, each - * replacement must be made before all others which follow it - * at later string indices in the input document. - * - * Sorting avoid making out-of-order replacements which - * can lead to mangled output, partially-duplicated - * attributes, and overwritten attributes. - */ - usort( $this->lexical_updates, array( self::class, 'sort_start_ascending' ) ); - - $bytes_already_copied = 0; - $output_buffer = ''; - foreach ( $this->lexical_updates as $diff ) { - $shift = strlen( $diff->text ) - ( $diff->end - $diff->start ); - - // Adjust the cursor position by however much an update affects it. - if ( $diff->start <= $this->bytes_already_parsed ) { - $this->bytes_already_parsed += $shift; - } - - // Accumulate shift of the given pointer within this function call. - if ( $diff->start <= $shift_this_point ) { - $accumulated_shift_for_given_point += $shift; - } - - $output_buffer .= substr( $this->html, $bytes_already_copied, $diff->start - $bytes_already_copied ); - $output_buffer .= $diff->text; - $bytes_already_copied = $diff->end; - } - - $this->html = $output_buffer . substr( $this->html, $bytes_already_copied ); - - /* - * Adjust bookmark locations to account for how the text - * replacements adjust offsets in the input document. - */ - foreach ( $this->bookmarks as $bookmark_name => $bookmark ) { - /* - * Each lexical update which appears before the bookmark's endpoints - * might shift the offsets for those endpoints. Loop through each change - * and accumulate the total shift for each bookmark, then apply that - * shift after tallying the full delta. - */ - $head_delta = 0; - $tail_delta = 0; - - foreach ( $this->lexical_updates as $diff ) { - if ( $bookmark->start < $diff->start && $bookmark->end < $diff->start ) { - break; - } - - if ( $bookmark->start >= $diff->start && $bookmark->end < $diff->end ) { - $this->release_bookmark( $bookmark_name ); - continue 2; - } - - $delta = strlen( $diff->text ) - ( $diff->end - $diff->start ); - - if ( $bookmark->start >= $diff->start ) { - $head_delta += $delta; - } - - if ( $bookmark->end >= $diff->end ) { - $tail_delta += $delta; - } - } - - $bookmark->start += $head_delta; - $bookmark->end += $tail_delta; - } - - $this->lexical_updates = array(); - - return $accumulated_shift_for_given_point; - } - - /** - * Checks whether a bookmark with the given name exists. - * - * @since 6.3.0 - * - * @param string $bookmark_name Name to identify a bookmark that potentially exists. - * @return bool Whether that bookmark exists. - */ - public function has_bookmark( $bookmark_name ) { - return array_key_exists( $bookmark_name, $this->bookmarks ); - } - - /** - * Move the internal cursor in the Tag Processor to a given bookmark's location. - * - * In order to prevent accidental infinite loops, there's a - * maximum limit on the number of times seek() can be called. - * - * @since 6.2.0 - * - * @param string $bookmark_name Jump to the place in the document identified by this bookmark name. - * @return bool Whether the internal cursor was successfully moved to the bookmark's location. - */ - public function seek( $bookmark_name ) { - if ( ! array_key_exists( $bookmark_name, $this->bookmarks ) ) { - _doing_it_wrong( - __METHOD__, - __( 'Unknown bookmark name.' ), - '6.2.0' - ); - return false; - } - - if ( ++$this->seek_count > self::MAX_SEEK_OPS ) { - _doing_it_wrong( - __METHOD__, - __( 'Too many calls to seek() - this can lead to performance issues.' ), - '6.2.0' - ); - return false; - } - - // Flush out any pending updates to the document. - $this->get_updated_html(); - - // Point this tag processor before the sought tag opener and consume it. - $this->bytes_already_parsed = $this->bookmarks[ $bookmark_name ]->start; - return $this->next_tag( array( 'tag_closers' => 'visit' ) ); - } - - /** - * Compare two WP_HTML_Text_Replacement objects. - * - * @since 6.2.0 - * - * @param WP_HTML_Text_Replacement $a First attribute update. - * @param WP_HTML_Text_Replacement $b Second attribute update. - * @return int Comparison value for string order. - */ - private static function sort_start_ascending( $a, $b ) { - $by_start = $a->start - $b->start; - if ( 0 !== $by_start ) { - return $by_start; - } - - $by_text = isset( $a->text, $b->text ) ? strcmp( $a->text, $b->text ) : 0; - if ( 0 !== $by_text ) { - return $by_text; - } - - /* - * This code should be unreachable, because it implies the two replacements - * start at the same location and contain the same text. - */ - return $a->end - $b->end; - } - - /** - * Return the enqueued value for a given attribute, if one exists. - * - * Enqueued updates can take different data types: - * - If an update is enqueued and is boolean, the return will be `true` - * - If an update is otherwise enqueued, the return will be the string value of that update. - * - If an attribute is enqueued to be removed, the return will be `null` to indicate that. - * - If no updates are enqueued, the return will be `false` to differentiate from "removed." - * - * @since 6.2.0 - * - * @param string $comparable_name The attribute name in its comparable form. - * @return string|boolean|null Value of enqueued update if present, otherwise false. - */ - private function get_enqueued_attribute_value( $comparable_name ) { - if ( ! isset( $this->lexical_updates[ $comparable_name ] ) ) { - return false; - } - - $enqueued_text = $this->lexical_updates[ $comparable_name ]->text; - - // Removed attributes erase the entire span. - if ( '' === $enqueued_text ) { - return null; - } - - /* - * Boolean attribute updates are just the attribute name without a corresponding value. - * - * This value might differ from the given comparable name in that there could be leading - * or trailing whitespace, and that the casing follows the name given in `set_attribute`. - * - * Example: - * - * $p->set_attribute( 'data-TEST-id', 'update' ); - * 'update' === $p->get_enqueued_attribute_value( 'data-test-id' ); - * - * Detect this difference based on the absence of the `=`, which _must_ exist in any - * attribute containing a value, e.g. ``. - * ¹ ² - * 1. Attribute with a string value. - * 2. Boolean attribute whose value is `true`. - */ - $equals_at = strpos( $enqueued_text, '=' ); - if ( false === $equals_at ) { - return true; - } - - /* - * Finally, a normal update's value will appear after the `=` and - * be double-quoted, as performed incidentally by `set_attribute`. - * - * e.g. `type="text"` - * ¹² ³ - * 1. Equals is here. - * 2. Double-quoting starts one after the equals sign. - * 3. Double-quoting ends at the last character in the update. - */ - $enqueued_value = substr( $enqueued_text, $equals_at + 2, -1 ); - return html_entity_decode( $enqueued_value ); - } - - /** - * Returns the value of a requested attribute from a matched tag opener if that attribute exists. - * - * Example: - * - * $p = new WP_HTML_Tag_Processor( '
Test
' ); - * $p->next_tag( array( 'class_name' => 'test' ) ) === true; - * $p->get_attribute( 'data-test-id' ) === '14'; - * $p->get_attribute( 'enabled' ) === true; - * $p->get_attribute( 'aria-label' ) === null; - * - * $p->next_tag() === false; - * $p->get_attribute( 'class' ) === null; - * - * @since 6.2.0 - * - * @param string $name Name of attribute whose value is requested. - * @return string|true|null Value of attribute or `null` if not available. Boolean attributes return `true`. - */ - public function get_attribute( $name ) { - if ( null === $this->tag_name_starts_at ) { - return null; - } - - $comparable = strtolower( $name ); - - /* - * For every attribute other than `class` it's possible to perform a quick check if - * there's an enqueued lexical update whose value takes priority over what's found in - * the input document. - * - * The `class` attribute is special though because of the exposed helpers `add_class` - * and `remove_class`. These form a builder for the `class` attribute, so an additional - * check for enqueued class changes is required in addition to the check for any enqueued - * attribute values. If any exist, those enqueued class changes must first be flushed out - * into an attribute value update. - */ - if ( 'class' === $name ) { - $this->class_name_updates_to_attributes_updates(); - } - - // Return any enqueued attribute value updates if they exist. - $enqueued_value = $this->get_enqueued_attribute_value( $comparable ); - if ( false !== $enqueued_value ) { - return $enqueued_value; - } - - if ( ! isset( $this->attributes[ $comparable ] ) ) { - return null; - } - - $attribute = $this->attributes[ $comparable ]; - - /* - * This flag distinguishes an attribute with no value - * from an attribute with an empty string value. For - * unquoted attributes this could look very similar. - * It refers to whether an `=` follows the name. - * - * e.g.
- * ¹ ² - * 1. Attribute `boolean-attribute` is `true`. - * 2. Attribute `empty-attribute` is `""`. - */ - if ( true === $attribute->is_true ) { - return true; - } - - $raw_value = substr( $this->html, $attribute->value_starts_at, $attribute->value_length ); - - return html_entity_decode( $raw_value ); - } - - /** - * Gets lowercase names of all attributes matching a given prefix in the current tag. - * - * Note that matching is case-insensitive. This is in accordance with the spec: - * - * > There must never be two or more attributes on - * > the same start tag whose names are an ASCII - * > case-insensitive match for each other. - * - HTML 5 spec - * - * Example: - * - * $p = new WP_HTML_Tag_Processor( '
Test
' ); - * $p->next_tag( array( 'class_name' => 'test' ) ) === true; - * $p->get_attribute_names_with_prefix( 'data-' ) === array( 'data-enabled', 'data-test-id' ); - * - * $p->next_tag() === false; - * $p->get_attribute_names_with_prefix( 'data-' ) === null; - * - * @since 6.2.0 - * - * @see https://html.spec.whatwg.org/multipage/syntax.html#attributes-2:ascii-case-insensitive - * - * @param string $prefix Prefix of requested attribute names. - * @return array|null List of attribute names, or `null` when no tag opener is matched. - */ - function get_attribute_names_with_prefix( $prefix ) { - if ( $this->is_closing_tag || null === $this->tag_name_starts_at ) { - return null; - } - - $comparable = strtolower( $prefix ); - - $matches = array(); - foreach ( array_keys( $this->attributes ) as $attr_name ) { - if ( str_starts_with( $attr_name, $comparable ) ) { - $matches[] = $attr_name; - } - } - return $matches; - } - - /** - * Returns the uppercase name of the matched tag. - * - * Example: - * - * $p = new WP_HTML_Tag_Processor( '
Test
' ); - * $p->next_tag() === true; - * $p->get_tag() === 'DIV'; - * - * $p->next_tag() === false; - * $p->get_tag() === null; - * - * @since 6.2.0 - * - * @return string|null Name of currently matched tag in input HTML, or `null` if none found. - */ - public function get_tag() { - if ( null === $this->tag_name_starts_at ) { - return null; - } - - $tag_name = substr( $this->html, $this->tag_name_starts_at, $this->tag_name_length ); - - return strtoupper( $tag_name ); - } - - /** - * Indicates if the currently matched tag contains the self-closing flag. - * - * No HTML elements ought to have the self-closing flag and for those, the self-closing - * flag will be ignored. For void elements this is benign because they "self close" - * automatically. For non-void HTML elements though problems will appear if someone - * intends to use a self-closing element in place of that element with an empty body. - * For HTML foreign elements and custom elements the self-closing flag determines if - * they self-close or not. - * - * This function does not determine if a tag is self-closing, - * but only if the self-closing flag is present in the syntax. - * - * @since 6.3.0 - * - * @return bool Whether the currently matched tag contains the self-closing flag. - */ - public function has_self_closing_flag() { - if ( ! $this->tag_name_starts_at ) { - return false; - } - - return '/' === $this->html[ $this->tag_ends_at - 1 ]; - } - - /** - * Indicates if the current tag token is a tag closer. - * - * Example: - * - * $p = new WP_HTML_Tag_Processor( '
' ); - * $p->next_tag( array( 'tag_name' => 'div', 'tag_closers' => 'visit' ) ); - * $p->is_tag_closer() === false; - * - * $p->next_tag( array( 'tag_name' => 'div', 'tag_closers' => 'visit' ) ); - * $p->is_tag_closer() === true; - * - * @since 6.2.0 - * - * @return bool Whether the current tag is a tag closer. - */ - public function is_tag_closer() { - return $this->is_closing_tag; - } - - /** - * Updates or creates a new attribute on the currently matched tag with the passed value. - * - * For boolean attributes special handling is provided: - * - When `true` is passed as the value, then only the attribute name is added to the tag. - * - When `false` is passed, the attribute gets removed if it existed before. - * - * For string attributes, the value is escaped using the `esc_attr` function. - * - * @since 6.2.0 - * @since 6.2.1 Fix: Only create a single update for multiple calls with case-variant attribute names. - * - * @param string $name The attribute name to target. - * @param string|bool $value The new attribute value. - * @return bool Whether an attribute value was set. - */ - public function set_attribute( $name, $value ) { - if ( $this->is_closing_tag || null === $this->tag_name_starts_at ) { - return false; - } - - /* - * WordPress rejects more characters than are strictly forbidden - * in HTML5. This is to prevent additional security risks deeper - * in the WordPress and plugin stack. Specifically the - * less-than (<) greater-than (>) and ampersand (&) aren't allowed. - * - * The use of a PCRE match enables looking for specific Unicode - * code points without writing a UTF-8 decoder. Whereas scanning - * for one-byte characters is trivial (with `strcspn`), scanning - * for the longer byte sequences would be more complicated. Given - * that this shouldn't be in the hot path for execution, it's a - * reasonable compromise in efficiency without introducing a - * noticeable impact on the overall system. - * - * @see https://html.spec.whatwg.org/#attributes-2 - * - * @TODO as the only regex pattern maybe we should take it out? are - * Unicode patterns available broadly in Core? - */ - if ( preg_match( - '~[' . - // Syntax-like characters. - '"\'>& The values "true" and "false" are not allowed on boolean attributes. - * > To represent a false value, the attribute has to be omitted altogether. - * - HTML5 spec, https://html.spec.whatwg.org/#boolean-attributes - */ - if ( false === $value ) { - return $this->remove_attribute( $name ); - } - - if ( true === $value ) { - $updated_attribute = $name; - } else { - $escaped_new_value = esc_attr( $value ); - $updated_attribute = "{$name}=\"{$escaped_new_value}\""; - } - - /* - * > There must never be two or more attributes on - * > the same start tag whose names are an ASCII - * > case-insensitive match for each other. - * - HTML 5 spec - * - * @see https://html.spec.whatwg.org/multipage/syntax.html#attributes-2:ascii-case-insensitive - */ - $comparable_name = strtolower( $name ); - - if ( isset( $this->attributes[ $comparable_name ] ) ) { - /* - * Update an existing attribute. - * - * Example – set attribute id to "new" in
: - * - *
- * ^-------------^ - * start end - * replacement: `id="new"` - * - * Result:
- */ - $existing_attribute = $this->attributes[ $comparable_name ]; - $this->lexical_updates[ $comparable_name ] = new WP_HTML_Text_Replacement( - $existing_attribute->start, - $existing_attribute->end, - $updated_attribute - ); - } else { - /* - * Create a new attribute at the tag's name end. - * - * Example – add attribute id="new" to
: - * - *
- * ^ - * start and end - * replacement: ` id="new"` - * - * Result:
- */ - $this->lexical_updates[ $comparable_name ] = new WP_HTML_Text_Replacement( - $this->tag_name_starts_at + $this->tag_name_length, - $this->tag_name_starts_at + $this->tag_name_length, - ' ' . $updated_attribute - ); - } - - /* - * Any calls to update the `class` attribute directly should wipe out any - * enqueued class changes from `add_class` and `remove_class`. - */ - if ( 'class' === $comparable_name && ! empty( $this->classname_updates ) ) { - $this->classname_updates = array(); - } - - return true; - } - - /** - * Remove an attribute from the currently-matched tag. - * - * @since 6.2.0 - * - * @param string $name The attribute name to remove. - * @return bool Whether an attribute was removed. - */ - public function remove_attribute( $name ) { - if ( $this->is_closing_tag ) { - return false; - } - - /* - * > There must never be two or more attributes on - * > the same start tag whose names are an ASCII - * > case-insensitive match for each other. - * - HTML 5 spec - * - * @see https://html.spec.whatwg.org/multipage/syntax.html#attributes-2:ascii-case-insensitive - */ - $name = strtolower( $name ); - - /* - * Any calls to update the `class` attribute directly should wipe out any - * enqueued class changes from `add_class` and `remove_class`. - */ - if ( 'class' === $name && count( $this->classname_updates ) !== 0 ) { - $this->classname_updates = array(); - } - - /* - * If updating an attribute that didn't exist in the input - * document, then remove the enqueued update and move on. - * - * For example, this might occur when calling `remove_attribute()` - * after calling `set_attribute()` for the same attribute - * and when that attribute wasn't originally present. - */ - if ( ! isset( $this->attributes[ $name ] ) ) { - if ( isset( $this->lexical_updates[ $name ] ) ) { - unset( $this->lexical_updates[ $name ] ); - } - return false; - } - - /* - * Removes an existing tag attribute. - * - * Example – remove the attribute id from
: - *
- * ^-------------^ - * start end - * replacement: `` - * - * Result:
- */ - $this->lexical_updates[ $name ] = new WP_HTML_Text_Replacement( - $this->attributes[ $name ]->start, - $this->attributes[ $name ]->end, - '' - ); - - // Removes any duplicated attributes if they were also present. - if ( null !== $this->duplicate_attributes && array_key_exists( $name, $this->duplicate_attributes ) ) { - foreach ( $this->duplicate_attributes[ $name ] as $attribute_token ) { - $this->lexical_updates[] = new WP_HTML_Text_Replacement( - $attribute_token->start, - $attribute_token->end, - '' - ); - } - } - - return true; - } - - /** - * Adds a new class name to the currently matched tag. - * - * @since 6.2.0 - * - * @param string $class_name The class name to add. - * @return bool Whether the class was set to be added. - */ - public function add_class( $class_name ) { - if ( $this->is_closing_tag ) { - return false; - } - - if ( null !== $this->tag_name_starts_at ) { - $this->classname_updates[ $class_name ] = self::ADD_CLASS; - } - - return true; - } - - /** - * Removes a class name from the currently matched tag. - * - * @since 6.2.0 - * - * @param string $class_name The class name to remove. - * @return bool Whether the class was set to be removed. - */ - public function remove_class( $class_name ) { - if ( $this->is_closing_tag ) { - return false; - } - - if ( null !== $this->tag_name_starts_at ) { - $this->classname_updates[ $class_name ] = self::REMOVE_CLASS; - } - - return true; - } - - /** - * Returns the string representation of the HTML Tag Processor. - * - * @since 6.2.0 - * - * @see WP_HTML_Tag_Processor::get_updated_html() - * - * @return string The processed HTML. - */ - public function __toString() { - return $this->get_updated_html(); - } - - /** - * Returns the string representation of the HTML Tag Processor. - * - * @since 6.2.0 - * @since 6.2.1 Shifts the internal cursor corresponding to the applied updates. - * - * @return string The processed HTML. - */ - public function get_updated_html() { - $requires_no_updating = 0 === count( $this->classname_updates ) && 0 === count( $this->lexical_updates ); - - /* - * When there is nothing more to update and nothing has already been - * updated, return the original document and avoid a string copy. - */ - if ( $requires_no_updating ) { - return $this->html; - } - - /* - * Keep track of the position right before the current tag. This will - * be necessary for reparsing the current tag after updating the HTML. - */ - $before_current_tag = $this->tag_name_starts_at - 1; - - /* - * 1. Apply the enqueued edits and update all the pointers to reflect those changes. - */ - $this->class_name_updates_to_attributes_updates(); - $before_current_tag += $this->apply_attributes_updates( $before_current_tag ); - - /* - * 2. Rewind to before the current tag and reparse to get updated attributes. - * - * At this point the internal cursor points to the end of the tag name. - * Rewind before the tag name starts so that it's as if the cursor didn't - * move; a call to `next_tag()` will reparse the recently-updated attributes - * and additional calls to modify the attributes will apply at this same - * location. - * - *

Previous HTMLMore HTML

- * ^ | back up by the length of the tag name plus the opening < - * \<-/ back up by strlen("em") + 1 ==> 3 - */ - - // Store existing state so it can be restored after reparsing. - $previous_parsed_byte_count = $this->bytes_already_parsed; - $previous_query = $this->last_query; - - // Reparse attributes. - $this->bytes_already_parsed = $before_current_tag; - $this->next_tag(); - - // Restore previous state. - $this->bytes_already_parsed = $previous_parsed_byte_count; - $this->parse_query( $previous_query ); - - return $this->html; - } - - /** - * Parses tag query input into internal search criteria. - * - * @since 6.2.0 - * - * @param array|string|null $query { - * Optional. Which tag name to find, having which class, etc. Default is to find any tag. - * - * @type string|null $tag_name Which tag to find, or `null` for "any tag." - * @type int|null $match_offset Find the Nth tag matching all search criteria. - * 1 for "first" tag, 3 for "third," etc. - * Defaults to first tag. - * @type string|null $class_name Tag must contain this class name to match. - * @type string $tag_closers "visit" or "skip": whether to stop on tag closers, e.g.
. - * } - */ - private function parse_query( $query ) { - if ( null !== $query && $query === $this->last_query ) { - return; - } - - $this->last_query = $query; - $this->sought_tag_name = null; - $this->sought_class_name = null; - $this->sought_match_offset = 1; - $this->stop_on_tag_closers = false; - - // A single string value means "find the tag of this name". - if ( is_string( $query ) ) { - $this->sought_tag_name = $query; - return; - } - - // An empty query parameter applies no restrictions on the search. - if ( null === $query ) { - return; - } - - // If not using the string interface, an associative array is required. - if ( ! is_array( $query ) ) { - _doing_it_wrong( - __METHOD__, - __( 'The query argument must be an array or a tag name.' ), - '6.2.0' - ); - return; - } - - if ( isset( $query['tag_name'] ) && is_string( $query['tag_name'] ) ) { - $this->sought_tag_name = $query['tag_name']; - } - - if ( isset( $query['class_name'] ) && is_string( $query['class_name'] ) ) { - $this->sought_class_name = $query['class_name']; - } - - if ( isset( $query['match_offset'] ) && is_int( $query['match_offset'] ) && 0 < $query['match_offset'] ) { - $this->sought_match_offset = $query['match_offset']; - } - - if ( isset( $query['tag_closers'] ) ) { - $this->stop_on_tag_closers = 'visit' === $query['tag_closers']; - } - } - - - /** - * Checks whether a given tag and its attributes match the search criteria. - * - * @since 6.2.0 - * - * @return boolean Whether the given tag and its attribute match the search criteria. - */ - private function matches() { - if ( $this->is_closing_tag && ! $this->stop_on_tag_closers ) { - return false; - } - - // Does the tag name match the requested tag name in a case-insensitive manner? - if ( null !== $this->sought_tag_name ) { - /* - * String (byte) length lookup is fast. If they aren't the - * same length then they can't be the same string values. - */ - if ( strlen( $this->sought_tag_name ) !== $this->tag_name_length ) { - return false; - } - - /* - * Check each character to determine if they are the same. - * Defer calls to `strtoupper()` to avoid them when possible. - * Calling `strcasecmp()` here tested slowed than comparing each - * character, so unless benchmarks show otherwise, it should - * not be used. - * - * It's expected that most of the time that this runs, a - * lower-case tag name will be supplied and the input will - * contain lower-case tag names, thus normally bypassing - * the case comparison code. - */ - for ( $i = 0; $i < $this->tag_name_length; $i++ ) { - $html_char = $this->html[ $this->tag_name_starts_at + $i ]; - $tag_char = $this->sought_tag_name[ $i ]; - - if ( $html_char !== $tag_char && strtoupper( $html_char ) !== $tag_char ) { - return false; - } - } - } - - $needs_class_name = null !== $this->sought_class_name; - - if ( $needs_class_name && ! isset( $this->attributes['class'] ) ) { - return false; - } - - /* - * Match byte-for-byte (case-sensitive and encoding-form-sensitive) on the class name. - * - * This will overlook certain classes that exist in other lexical variations - * than was supplied to the search query, but requires more complicated searching. - */ - if ( $needs_class_name ) { - $class_start = $this->attributes['class']->value_starts_at; - $class_end = $class_start + $this->attributes['class']->value_length; - $class_at = $class_start; - - /* - * Ensure that boundaries surround the class name to avoid matching on - * substrings of a longer name. For example, the sequence "not-odd" - * should not match for the class "odd" even though "odd" is found - * within the class attribute text. - * - * See https://html.spec.whatwg.org/#attributes-3 - * See https://html.spec.whatwg.org/#space-separated-tokens - */ - while ( - // phpcs:ignore WordPress.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition - false !== ( $class_at = strpos( $this->html, $this->sought_class_name, $class_at ) ) && - $class_at < $class_end - ) { - /* - * Verify this class starts at a boundary. - */ - if ( $class_at > $class_start ) { - $character = $this->html[ $class_at - 1 ]; - - if ( ' ' !== $character && "\t" !== $character && "\f" !== $character && "\r" !== $character && "\n" !== $character ) { - $class_at += strlen( $this->sought_class_name ); - continue; - } - } - - /* - * Verify this class ends at a boundary as well. - */ - if ( $class_at + strlen( $this->sought_class_name ) < $class_end ) { - $character = $this->html[ $class_at + strlen( $this->sought_class_name ) ]; - - if ( ' ' !== $character && "\t" !== $character && "\f" !== $character && "\r" !== $character && "\n" !== $character ) { - $class_at += strlen( $this->sought_class_name ); - continue; - } - } - - return true; - } - - return false; - } - - return true; - } -} diff --git a/lib/compat/wordpress-6.4/block-patterns.php b/lib/compat/wordpress-6.4/block-patterns.php index 922dea910b47a..65c31fb7a22af 100644 --- a/lib/compat/wordpress-6.4/block-patterns.php +++ b/lib/compat/wordpress-6.4/block-patterns.php @@ -16,7 +16,7 @@ */ function gutenberg_register_taxonomy_patterns() { $args = array( - 'public' => true, + 'public' => false, 'publicly_queryable' => false, 'hierarchical' => false, 'labels' => array( diff --git a/lib/compat/wordpress-6.4/html-api/class-gutenberg-html-tag-processor-6-4.php b/lib/compat/wordpress-6.4/html-api/class-gutenberg-html-tag-processor-6-4.php index 13f0ec5fad479..509d2c1a2c9ab 100644 --- a/lib/compat/wordpress-6.4/html-api/class-gutenberg-html-tag-processor-6-4.php +++ b/lib/compat/wordpress-6.4/html-api/class-gutenberg-html-tag-processor-6-4.php @@ -2270,6 +2270,7 @@ public function __toString() { * * @since 6.2.0 * @since 6.2.1 Shifts the internal cursor corresponding to the applied updates. + * @since 6.4.0 No longer calls subclass method `next_tag()` after updating HTML. * * @return string The processed HTML. */ @@ -2303,24 +2304,29 @@ public function get_updated_html() { * Rewind before the tag name starts so that it's as if the cursor didn't * move; a call to `next_tag()` will reparse the recently-updated attributes * and additional calls to modify the attributes will apply at this same - * location. + * location, but in order to avoid issues with subclasses that might add + * behaviors to `next_tag()`, the internal methods should be called here + * instead. + * + * It's important to note that in this specific place there will be no change + * because the processor was already at a tag when this was called and it's + * rewinding only to the beginning of this very tag before reprocessing it + * and its attributes. * *

Previous HTMLMore HTML

- * ^ | back up by the length of the tag name plus the opening < - * \<-/ back up by strlen("em") + 1 ==> 3 + * ↑ │ back up by the length of the tag name plus the opening < + * └←─┘ back up by strlen("em") + 1 ==> 3 */ - - // Store existing state so it can be restored after reparsing. - $previous_parsed_byte_count = $this->bytes_already_parsed; - $previous_query = $this->last_query; - - // Reparse attributes. $this->bytes_already_parsed = $before_current_tag; - $this->next_tag(); + $this->parse_next_tag(); + // Reparse the attributes. + while ( $this->parse_next_attribute() ) { + continue; + } - // Restore previous state. - $this->bytes_already_parsed = $previous_parsed_byte_count; - $this->parse_query( $previous_query ); + $tag_ends_at = strpos( $this->html, '>', $this->bytes_already_parsed ); + $this->tag_ends_at = $tag_ends_at; + $this->bytes_already_parsed = $tag_ends_at; return $this->html; } diff --git a/lib/experimental/data-views.php b/lib/experimental/data-views.php index e46fe36ba06fe..e0346184ffc21 100644 --- a/lib/experimental/data-views.php +++ b/lib/experimental/data-views.php @@ -9,6 +9,10 @@ * Registers the `wp_dataviews` post type and the `wp_dataviews_type` taxonomy. */ function _gutenberg_register_data_views_post_type() { + $gutenberg_experiments = get_option( 'gutenberg-experiments' ); + if ( empty( $gutenberg_experiments ) || ! array_key_exists( 'gutenberg-dataviews', $gutenberg_experiments ) ) { + return; + } register_post_type( 'wp_dataviews', array( diff --git a/lib/load.php b/lib/load.php index 2b178af5fb9bf..5740e43b5f76a 100644 --- a/lib/load.php +++ b/lib/load.php @@ -81,7 +81,17 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/compat/plugin/edit-site-routes-backwards-compat.php'; require __DIR__ . '/compat/plugin/footnotes.php'; +/* + * There are upstream updates to the Tag Processor that may not appear if Gutenberg is running + * a version of WordPress newer than 6.3 and older than the latest `trunk`. This file should + * always be loaded so that Gutenberg code can run the newest version of the Tag Processor. + */ require __DIR__ . '/compat/wordpress-6.4/html-api/class-gutenberg-html-tag-processor-6-4.php'; + +/* + * The HTML Processor appeared after WordPress 6.3. If Gutenberg is running on a version of + * WordPress before it was introduced, these verbatim Core files will be missing. + */ if ( ! class_exists( 'WP_HTML_Processor' ) ) { require __DIR__ . '/compat/wordpress-6.4/html-api/class-wp-html-active-formatting-elements.php'; require __DIR__ . '/compat/wordpress-6.4/html-api/class-wp-html-open-elements.php'; @@ -94,7 +104,6 @@ function gutenberg_is_experiment_enabled( $name ) { // WordPress 6.3 compat. require __DIR__ . '/compat/wordpress-6.3/get-global-styles-and-settings.php'; require __DIR__ . '/compat/wordpress-6.3/block-template-utils.php'; -require __DIR__ . '/compat/wordpress-6.3/html-api/class-gutenberg-html-tag-processor-6-3.php'; require __DIR__ . '/compat/wordpress-6.3/script-loader.php'; require __DIR__ . '/compat/wordpress-6.3/blocks.php'; require __DIR__ . '/compat/wordpress-6.3/navigation-fallback.php'; diff --git a/package-lock.json b/package-lock.json index 0fa085c6fc603..31325d1b1343d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "gutenberg", - "version": "16.9.0", + "version": "17.0.0-rc.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "gutenberg", - "version": "16.9.0", + "version": "17.0.0-rc.1", "hasInstallScript": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -54329,7 +54329,7 @@ }, "packages/a11y": { "name": "@wordpress/a11y", - "version": "3.44.0", + "version": "3.45.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54342,7 +54342,7 @@ }, "packages/annotations": { "name": "@wordpress/annotations", - "version": "2.44.0", + "version": "2.45.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54370,7 +54370,7 @@ }, "packages/api-fetch": { "name": "@wordpress/api-fetch", - "version": "6.41.0", + "version": "6.42.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54383,7 +54383,7 @@ }, "packages/autop": { "name": "@wordpress/autop", - "version": "3.44.0", + "version": "3.45.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0" @@ -54394,7 +54394,7 @@ }, "packages/babel-plugin-import-jsx-pragma": { "name": "@wordpress/babel-plugin-import-jsx-pragma", - "version": "4.27.0", + "version": "4.28.0", "dev": true, "license": "GPL-2.0-or-later", "engines": { @@ -54406,7 +54406,7 @@ }, "packages/babel-plugin-makepot": { "name": "@wordpress/babel-plugin-makepot", - "version": "5.28.0", + "version": "5.29.0", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -54423,7 +54423,7 @@ }, "packages/babel-preset-default": { "name": "@wordpress/babel-preset-default", - "version": "7.28.0", + "version": "7.29.0", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -54446,13 +54446,13 @@ }, "packages/base-styles": { "name": "@wordpress/base-styles", - "version": "4.35.0", + "version": "4.36.0", "dev": true, "license": "GPL-2.0-or-later" }, "packages/blob": { "name": "@wordpress/blob", - "version": "3.44.0", + "version": "3.45.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0" @@ -54463,7 +54463,7 @@ }, "packages/block-directory": { "name": "@wordpress/block-directory", - "version": "4.21.0", + "version": "4.22.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54498,7 +54498,7 @@ }, "packages/block-editor": { "name": "@wordpress/block-editor", - "version": "12.12.0", + "version": "12.13.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54595,7 +54595,7 @@ }, "packages/block-library": { "name": "@wordpress/block-library", - "version": "8.21.0", + "version": "8.22.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54657,7 +54657,7 @@ }, "packages/block-serialization-default-parser": { "name": "@wordpress/block-serialization-default-parser", - "version": "4.44.0", + "version": "4.45.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0" @@ -54668,7 +54668,7 @@ }, "packages/block-serialization-spec-parser": { "name": "@wordpress/block-serialization-spec-parser", - "version": "4.44.0", + "version": "4.45.0", "license": "GPL-2.0-or-later", "dependencies": { "pegjs": "^0.10.0", @@ -54680,7 +54680,7 @@ }, "packages/blocks": { "name": "@wordpress/blocks", - "version": "12.21.0", + "version": "12.22.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54728,7 +54728,7 @@ }, "packages/browserslist-config": { "name": "@wordpress/browserslist-config", - "version": "5.27.0", + "version": "5.28.0", "dev": true, "license": "GPL-2.0-or-later", "engines": { @@ -54737,7 +54737,7 @@ }, "packages/commands": { "name": "@wordpress/commands", - "version": "0.15.0", + "version": "0.16.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54762,7 +54762,7 @@ }, "packages/components": { "name": "@wordpress/components", - "version": "25.10.0", + "version": "25.11.0", "license": "GPL-2.0-or-later", "dependencies": { "@ariakit/react": "^0.3.5", @@ -54869,7 +54869,7 @@ }, "packages/compose": { "name": "@wordpress/compose", - "version": "6.21.0", + "version": "6.22.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54895,7 +54895,7 @@ }, "packages/core-commands": { "name": "@wordpress/core-commands", - "version": "0.13.0", + "version": "0.14.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54920,7 +54920,7 @@ }, "packages/core-data": { "name": "@wordpress/core-data", - "version": "6.21.0", + "version": "6.22.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54964,7 +54964,7 @@ }, "packages/create-block": { "name": "@wordpress/create-block", - "version": "4.28.0", + "version": "4.29.0", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -54992,13 +54992,13 @@ }, "packages/create-block-tutorial-template": { "name": "@wordpress/create-block-tutorial-template", - "version": "2.32.0", + "version": "2.33.0", "dev": true, "license": "GPL-2.0-or-later" }, "packages/customize-widgets": { "name": "@wordpress/customize-widgets", - "version": "4.21.0", + "version": "4.22.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55035,7 +55035,7 @@ }, "packages/data": { "name": "@wordpress/data", - "version": "9.14.0", + "version": "9.15.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55063,7 +55063,7 @@ }, "packages/data-controls": { "name": "@wordpress/data-controls", - "version": "3.13.0", + "version": "3.14.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55080,7 +55080,7 @@ }, "packages/date": { "name": "@wordpress/date", - "version": "4.44.0", + "version": "4.45.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55094,7 +55094,7 @@ }, "packages/dependency-extraction-webpack-plugin": { "name": "@wordpress/dependency-extraction-webpack-plugin", - "version": "4.27.0", + "version": "4.28.0", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -55110,7 +55110,7 @@ }, "packages/deprecated": { "name": "@wordpress/deprecated", - "version": "3.44.0", + "version": "3.45.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55122,7 +55122,7 @@ }, "packages/docgen": { "name": "@wordpress/docgen", - "version": "1.53.0", + "version": "1.54.0", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -55140,7 +55140,7 @@ }, "packages/dom": { "name": "@wordpress/dom", - "version": "3.44.0", + "version": "3.45.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55152,7 +55152,7 @@ }, "packages/dom-ready": { "name": "@wordpress/dom-ready", - "version": "3.44.0", + "version": "3.45.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0" @@ -55163,7 +55163,7 @@ }, "packages/e2e-test-utils": { "name": "@wordpress/e2e-test-utils", - "version": "10.15.0", + "version": "10.16.0", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -55185,7 +55185,7 @@ }, "packages/e2e-test-utils-playwright": { "name": "@wordpress/e2e-test-utils-playwright", - "version": "0.12.0", + "version": "0.13.0", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -55207,7 +55207,7 @@ }, "packages/e2e-tests": { "name": "@wordpress/e2e-tests", - "version": "7.15.0", + "version": "7.16.0", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -55245,7 +55245,7 @@ }, "packages/edit-post": { "name": "@wordpress/edit-post", - "version": "7.21.0", + "version": "7.22.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55293,7 +55293,7 @@ }, "packages/edit-site": { "name": "@wordpress/edit-site", - "version": "5.21.0", + "version": "5.22.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55358,7 +55358,7 @@ }, "packages/edit-widgets": { "name": "@wordpress/edit-widgets", - "version": "5.21.0", + "version": "5.22.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55400,7 +55400,7 @@ }, "packages/editor": { "name": "@wordpress/editor", - "version": "13.21.0", + "version": "13.22.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55450,7 +55450,7 @@ }, "packages/element": { "name": "@wordpress/element", - "version": "5.21.0", + "version": "5.22.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55468,7 +55468,7 @@ }, "packages/env": { "name": "@wordpress/env", - "version": "8.10.0", + "version": "8.11.0", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -55581,7 +55581,7 @@ }, "packages/escape-html": { "name": "@wordpress/escape-html", - "version": "2.44.0", + "version": "2.45.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0" @@ -55592,7 +55592,7 @@ }, "packages/eslint-plugin": { "name": "@wordpress/eslint-plugin", - "version": "17.1.0", + "version": "17.2.0", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -55635,7 +55635,7 @@ }, "packages/format-library": { "name": "@wordpress/format-library", - "version": "4.21.0", + "version": "4.22.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55661,7 +55661,7 @@ }, "packages/hooks": { "name": "@wordpress/hooks", - "version": "3.44.0", + "version": "3.45.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0" @@ -55672,7 +55672,7 @@ }, "packages/html-entities": { "name": "@wordpress/html-entities", - "version": "3.44.0", + "version": "3.45.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0" @@ -55683,7 +55683,7 @@ }, "packages/i18n": { "name": "@wordpress/i18n", - "version": "4.44.0", + "version": "4.45.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55702,7 +55702,7 @@ }, "packages/icons": { "name": "@wordpress/icons", - "version": "9.35.0", + "version": "9.36.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55715,7 +55715,7 @@ }, "packages/interactivity": { "name": "@wordpress/interactivity", - "version": "2.5.0", + "version": "2.6.0", "license": "GPL-2.0-or-later", "dependencies": { "@preact/signals": "^1.1.3", @@ -55728,7 +55728,7 @@ }, "packages/interface": { "name": "@wordpress/interface", - "version": "5.21.0", + "version": "5.22.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55755,7 +55755,7 @@ }, "packages/is-shallow-equal": { "name": "@wordpress/is-shallow-equal", - "version": "4.44.0", + "version": "4.45.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0" @@ -55766,7 +55766,7 @@ }, "packages/jest-console": { "name": "@wordpress/jest-console", - "version": "7.15.0", + "version": "7.16.0", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -55782,7 +55782,7 @@ }, "packages/jest-preset-default": { "name": "@wordpress/jest-preset-default", - "version": "11.15.0", + "version": "11.16.0", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -55799,7 +55799,7 @@ }, "packages/jest-puppeteer-axe": { "name": "@wordpress/jest-puppeteer-axe", - "version": "6.15.0", + "version": "6.16.0", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -55821,7 +55821,7 @@ }, "packages/keyboard-shortcuts": { "name": "@wordpress/keyboard-shortcuts", - "version": "4.21.0", + "version": "4.22.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55839,7 +55839,7 @@ }, "packages/keycodes": { "name": "@wordpress/keycodes", - "version": "3.44.0", + "version": "3.45.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55852,7 +55852,7 @@ }, "packages/lazy-import": { "name": "@wordpress/lazy-import", - "version": "1.31.0", + "version": "1.32.0", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -55866,7 +55866,7 @@ }, "packages/list-reusable-blocks": { "name": "@wordpress/list-reusable-blocks", - "version": "4.21.0", + "version": "4.22.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55887,7 +55887,7 @@ }, "packages/media-utils": { "name": "@wordpress/media-utils", - "version": "4.35.0", + "version": "4.36.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55902,7 +55902,7 @@ }, "packages/notices": { "name": "@wordpress/notices", - "version": "4.12.0", + "version": "4.13.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55918,7 +55918,7 @@ }, "packages/npm-package-json-lint-config": { "name": "@wordpress/npm-package-json-lint-config", - "version": "4.29.0", + "version": "4.30.0", "dev": true, "license": "GPL-2.0-or-later", "engines": { @@ -55930,7 +55930,7 @@ }, "packages/nux": { "name": "@wordpress/nux", - "version": "8.6.0", + "version": "8.7.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55953,10 +55953,11 @@ }, "packages/patterns": { "name": "@wordpress/patterns", - "version": "1.5.0", + "version": "1.6.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", + "@wordpress/a11y": "file:../a11y", "@wordpress/block-editor": "file:../block-editor", "@wordpress/blocks": "file:../blocks", "@wordpress/components": "file:../components", @@ -55981,7 +55982,7 @@ }, "packages/plugins": { "name": "@wordpress/plugins", - "version": "6.12.0", + "version": "6.13.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -56003,7 +56004,7 @@ }, "packages/postcss-plugins-preset": { "name": "@wordpress/postcss-plugins-preset", - "version": "4.28.0", + "version": "4.29.0", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -56019,7 +56020,7 @@ }, "packages/postcss-themes": { "name": "@wordpress/postcss-themes", - "version": "5.27.0", + "version": "5.28.0", "dev": true, "license": "GPL-2.0-or-later", "engines": { @@ -56031,7 +56032,7 @@ }, "packages/preferences": { "name": "@wordpress/preferences", - "version": "3.21.0", + "version": "3.22.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -56053,7 +56054,7 @@ }, "packages/preferences-persistence": { "name": "@wordpress/preferences-persistence", - "version": "1.36.0", + "version": "1.37.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -56065,7 +56066,7 @@ }, "packages/prettier-config": { "name": "@wordpress/prettier-config", - "version": "3.1.0", + "version": "3.2.0", "dev": true, "license": "GPL-2.0-or-later", "engines": { @@ -56077,7 +56078,7 @@ }, "packages/primitives": { "name": "@wordpress/primitives", - "version": "3.42.0", + "version": "3.43.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -56090,7 +56091,7 @@ }, "packages/priority-queue": { "name": "@wordpress/priority-queue", - "version": "2.44.0", + "version": "2.45.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -56102,7 +56103,7 @@ }, "packages/private-apis": { "name": "@wordpress/private-apis", - "version": "0.26.0", + "version": "0.27.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0" @@ -56113,7 +56114,7 @@ }, "packages/project-management-automation": { "name": "@wordpress/project-management-automation", - "version": "1.43.0", + "version": "1.44.0", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -56126,7 +56127,7 @@ }, "packages/react-i18n": { "name": "@wordpress/react-i18n", - "version": "3.42.0", + "version": "3.43.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -56274,7 +56275,7 @@ }, "packages/readable-js-assets-webpack-plugin": { "name": "@wordpress/readable-js-assets-webpack-plugin", - "version": "2.27.0", + "version": "2.28.0", "dev": true, "license": "GPL-2.0-or-later", "engines": { @@ -56286,7 +56287,7 @@ }, "packages/redux-routine": { "name": "@wordpress/redux-routine", - "version": "4.44.0", + "version": "4.45.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -56330,7 +56331,7 @@ }, "packages/reusable-blocks": { "name": "@wordpress/reusable-blocks", - "version": "4.21.0", + "version": "4.22.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -56356,7 +56357,7 @@ }, "packages/rich-text": { "name": "@wordpress/rich-text", - "version": "6.21.0", + "version": "6.22.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -56380,7 +56381,7 @@ }, "packages/router": { "name": "@wordpress/router", - "version": "0.13.0", + "version": "0.14.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -56398,7 +56399,7 @@ }, "packages/scripts": { "name": "@wordpress/scripts", - "version": "26.15.0", + "version": "26.16.0", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -56476,7 +56477,7 @@ }, "packages/server-side-render": { "name": "@wordpress/server-side-render", - "version": "4.21.0", + "version": "4.22.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -56501,7 +56502,7 @@ }, "packages/shortcode": { "name": "@wordpress/shortcode", - "version": "3.44.0", + "version": "3.45.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -56513,7 +56514,7 @@ }, "packages/style-engine": { "name": "@wordpress/style-engine", - "version": "1.27.0", + "version": "1.28.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -56525,7 +56526,7 @@ }, "packages/stylelint-config": { "name": "@wordpress/stylelint-config", - "version": "21.27.0", + "version": "21.28.0", "dev": true, "license": "MIT", "dependencies": { @@ -56541,7 +56542,7 @@ }, "packages/sync": { "name": "@wordpress/sync", - "version": "0.6.0", + "version": "0.7.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -56561,7 +56562,7 @@ }, "packages/token-list": { "name": "@wordpress/token-list", - "version": "2.44.0", + "version": "2.45.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0" @@ -56572,7 +56573,7 @@ }, "packages/undo-manager": { "name": "@wordpress/undo-manager", - "version": "0.4.0", + "version": "0.5.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -56584,7 +56585,7 @@ }, "packages/url": { "name": "@wordpress/url", - "version": "3.45.0", + "version": "3.46.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -56596,7 +56597,7 @@ }, "packages/viewport": { "name": "@wordpress/viewport", - "version": "5.21.0", + "version": "5.22.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -56613,7 +56614,7 @@ }, "packages/warning": { "name": "@wordpress/warning", - "version": "2.44.0", + "version": "2.45.0", "license": "GPL-2.0-or-later", "engines": { "node": ">=12" @@ -56621,7 +56622,7 @@ }, "packages/widgets": { "name": "@wordpress/widgets", - "version": "3.21.0", + "version": "3.22.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -56645,7 +56646,7 @@ }, "packages/wordcount": { "name": "@wordpress/wordcount", - "version": "3.44.0", + "version": "3.45.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0" @@ -70798,6 +70799,7 @@ "version": "file:packages/patterns", "requires": { "@babel/runtime": "^7.16.0", + "@wordpress/a11y": "file:../a11y", "@wordpress/block-editor": "file:../block-editor", "@wordpress/blocks": "file:../blocks", "@wordpress/components": "file:../components", diff --git a/package.json b/package.json index 3f9a56ed86dde..6155e483f0082 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "16.9.0", + "version": "17.0.0-rc.1", "private": true, "description": "A new WordPress editor experience.", "author": "The WordPress Contributors", diff --git a/packages/a11y/CHANGELOG.md b/packages/a11y/CHANGELOG.md index 5a967d1e4bd94..2be0c0b64bb14 100644 --- a/packages/a11y/CHANGELOG.md +++ b/packages/a11y/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 3.45.0 (2023-11-02) + ## 3.44.0 (2023-10-18) ## 3.43.0 (2023-10-05) diff --git a/packages/a11y/package.json b/packages/a11y/package.json index 722eb1e23dea4..a48168ac57578 100644 --- a/packages/a11y/package.json +++ b/packages/a11y/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/a11y", - "version": "3.44.0", + "version": "3.45.0", "description": "Accessibility (a11y) utilities for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/annotations/CHANGELOG.md b/packages/annotations/CHANGELOG.md index 0da1ca4afdc3b..a45627849418b 100644 --- a/packages/annotations/CHANGELOG.md +++ b/packages/annotations/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 2.45.0 (2023-11-02) + ## 2.44.0 (2023-10-18) ## 2.43.0 (2023-10-05) diff --git a/packages/annotations/package.json b/packages/annotations/package.json index 86ee1ba082e10..32b3eee0e2ecb 100644 --- a/packages/annotations/package.json +++ b/packages/annotations/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/annotations", - "version": "2.44.0", + "version": "2.45.0", "description": "Annotate content in the Gutenberg editor.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/api-fetch/CHANGELOG.md b/packages/api-fetch/CHANGELOG.md index 4237f4bd00122..5806a41860d5c 100644 --- a/packages/api-fetch/CHANGELOG.md +++ b/packages/api-fetch/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 6.42.0 (2023-11-02) + ## 6.41.0 (2023-10-18) ## 6.40.0 (2023-10-05) diff --git a/packages/api-fetch/package.json b/packages/api-fetch/package.json index 2ed7702ce68cc..e5413887a61ce 100644 --- a/packages/api-fetch/package.json +++ b/packages/api-fetch/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/api-fetch", - "version": "6.41.0", + "version": "6.42.0", "description": "Utility to make WordPress REST API requests.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/autop/CHANGELOG.md b/packages/autop/CHANGELOG.md index 50d7f9d386e0c..a3cba5718404b 100644 --- a/packages/autop/CHANGELOG.md +++ b/packages/autop/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 3.45.0 (2023-11-02) + ## 3.44.0 (2023-10-18) ## 3.43.0 (2023-10-05) diff --git a/packages/autop/package.json b/packages/autop/package.json index 3a49145972c01..13eb7cac02d54 100644 --- a/packages/autop/package.json +++ b/packages/autop/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/autop", - "version": "3.44.0", + "version": "3.45.0", "description": "WordPress's automatic paragraph functions `autop` and `removep`.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/babel-plugin-import-jsx-pragma/CHANGELOG.md b/packages/babel-plugin-import-jsx-pragma/CHANGELOG.md index 381cb865e99a2..292314b1c9fe1 100644 --- a/packages/babel-plugin-import-jsx-pragma/CHANGELOG.md +++ b/packages/babel-plugin-import-jsx-pragma/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 4.28.0 (2023-11-02) + ## 4.27.0 (2023-10-18) ## 4.26.0 (2023-10-05) diff --git a/packages/babel-plugin-import-jsx-pragma/package.json b/packages/babel-plugin-import-jsx-pragma/package.json index 040006bbcd178..00a621f31b1e0 100644 --- a/packages/babel-plugin-import-jsx-pragma/package.json +++ b/packages/babel-plugin-import-jsx-pragma/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/babel-plugin-import-jsx-pragma", - "version": "4.27.0", + "version": "4.28.0", "description": "Babel transform plugin for automatically injecting an import to be used as the pragma for the React JSX Transform plugin.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/babel-plugin-makepot/CHANGELOG.md b/packages/babel-plugin-makepot/CHANGELOG.md index b161e5bbc2ffc..0b57ac2d9b0a1 100644 --- a/packages/babel-plugin-makepot/CHANGELOG.md +++ b/packages/babel-plugin-makepot/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 5.29.0 (2023-11-02) + ## 5.28.0 (2023-10-18) ## 5.27.0 (2023-10-05) diff --git a/packages/babel-plugin-makepot/package.json b/packages/babel-plugin-makepot/package.json index a66d36503a462..4acc61c4819e6 100644 --- a/packages/babel-plugin-makepot/package.json +++ b/packages/babel-plugin-makepot/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/babel-plugin-makepot", - "version": "5.28.0", + "version": "5.29.0", "description": "WordPress Babel internationalization (i18n) plugin.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/babel-preset-default/CHANGELOG.md b/packages/babel-preset-default/CHANGELOG.md index 77c03f4514eec..e5a4c06b4c92b 100644 --- a/packages/babel-preset-default/CHANGELOG.md +++ b/packages/babel-preset-default/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 7.29.0 (2023-11-02) + ## 7.28.0 (2023-10-18) ## 7.27.0 (2023-10-05) diff --git a/packages/babel-preset-default/package.json b/packages/babel-preset-default/package.json index 720e2f1b8c109..dd38c349bb171 100644 --- a/packages/babel-preset-default/package.json +++ b/packages/babel-preset-default/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/babel-preset-default", - "version": "7.28.0", + "version": "7.29.0", "description": "Default Babel preset for WordPress development.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/base-styles/CHANGELOG.md b/packages/base-styles/CHANGELOG.md index 572b3cfeb94f0..b6135581dbd73 100644 --- a/packages/base-styles/CHANGELOG.md +++ b/packages/base-styles/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 4.36.0 (2023-11-02) + ## 4.35.0 (2023-10-18) ## 4.34.0 (2023-10-05) diff --git a/packages/base-styles/package.json b/packages/base-styles/package.json index d187f9721edd0..fe604a7cb0211 100644 --- a/packages/base-styles/package.json +++ b/packages/base-styles/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/base-styles", - "version": "4.35.0", + "version": "4.36.0", "description": "Base SCSS utilities and variables for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/blob/CHANGELOG.md b/packages/blob/CHANGELOG.md index 3d58360f7ff7a..8d88b3cc4062a 100644 --- a/packages/blob/CHANGELOG.md +++ b/packages/blob/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 3.45.0 (2023-11-02) + ## 3.44.0 (2023-10-18) ## 3.43.0 (2023-10-05) diff --git a/packages/blob/package.json b/packages/blob/package.json index 2692d3e0cfb41..c3e89fb991823 100644 --- a/packages/blob/package.json +++ b/packages/blob/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/blob", - "version": "3.44.0", + "version": "3.45.0", "description": "Blob utilities for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/block-directory/CHANGELOG.md b/packages/block-directory/CHANGELOG.md index 093371bb77936..eb3a68dd24cc8 100644 --- a/packages/block-directory/CHANGELOG.md +++ b/packages/block-directory/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 4.22.0 (2023-11-02) + ## 4.21.0 (2023-10-18) ## 4.20.0 (2023-10-05) diff --git a/packages/block-directory/package.json b/packages/block-directory/package.json index 11cb946ef9189..71018738fac08 100644 --- a/packages/block-directory/package.json +++ b/packages/block-directory/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/block-directory", - "version": "4.21.0", + "version": "4.22.0", "description": "Extend editor with block directory features to search, download and install blocks.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/block-editor/CHANGELOG.md b/packages/block-editor/CHANGELOG.md index 5c6e9a5602add..94e0306e17565 100644 --- a/packages/block-editor/CHANGELOG.md +++ b/packages/block-editor/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 12.13.0 (2023-11-02) + - Deprecated the `useSetting` function in favor of new `useSettings` one that can retrieve multiple settings at once ([#55337](https://github.com/WordPress/gutenberg/pull/55337)). ## 12.12.0 (2023-10-18) diff --git a/packages/block-editor/package.json b/packages/block-editor/package.json index 5abf843b85f51..baebc82408652 100644 --- a/packages/block-editor/package.json +++ b/packages/block-editor/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/block-editor", - "version": "12.12.0", + "version": "12.13.0", "description": "Generic block editor.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/block-editor/src/components/block-tools/back-compat.js b/packages/block-editor/src/components/block-tools/back-compat.js index 3597bc07b07f7..029419926e9ed 100644 --- a/packages/block-editor/src/components/block-tools/back-compat.js +++ b/packages/block-editor/src/components/block-tools/back-compat.js @@ -9,7 +9,7 @@ import deprecated from '@wordpress/deprecated'; * Internal dependencies */ import InsertionPoint, { InsertionPointOpenRef } from './insertion-point'; -import BlockPopover from './selected-block-popover'; +import BlockPopover from './selected-block-tools'; export default function BlockToolsBackCompat( { children } ) { const openRef = useContext( InsertionPointOpenRef ); diff --git a/packages/block-editor/src/components/block-tools/block-contextual-toolbar.js b/packages/block-editor/src/components/block-tools/block-contextual-toolbar.js index fcec9d56b24a8..7c85aaeabb983 100644 --- a/packages/block-editor/src/components/block-tools/block-contextual-toolbar.js +++ b/packages/block-editor/src/components/block-tools/block-contextual-toolbar.js @@ -8,6 +8,7 @@ import classnames from 'classnames'; */ import { __ } from '@wordpress/i18n'; import { + forwardRef, useLayoutEffect, useEffect, useRef, @@ -31,7 +32,10 @@ import BlockToolbar from '../block-toolbar'; import { store as blockEditorStore } from '../../store'; import { useHasAnyBlockControls } from '../block-controls/use-has-block-controls'; -function BlockContextualToolbar( { focusOnMount, isFixed, ...props } ) { +function UnforwardedBlockContextualToolbar( + { focusOnMount, isFixed, ...props }, + ref +) { // When the toolbar is fixed it can be collapsed const [ isCollapsed, setIsCollapsed ] = useState( false ); const toolbarButtonRef = useRef(); @@ -184,7 +188,9 @@ function BlockContextualToolbar( { focusOnMount, isFixed, ...props } ) { return ( +
+ +
+ + ); +} diff --git a/packages/block-editor/src/components/block-tools/index.js b/packages/block-editor/src/components/block-tools/index.js index 8e3b240838fd0..696299689d185 100644 --- a/packages/block-editor/src/components/block-tools/index.js +++ b/packages/block-editor/src/components/block-tools/index.js @@ -6,28 +6,48 @@ import { useViewportMatch } from '@wordpress/compose'; import { Popover } from '@wordpress/components'; import { __unstableUseShortcutEventMatch as useShortcutEventMatch } from '@wordpress/keyboard-shortcuts'; import { useRef } from '@wordpress/element'; +import { isUnmodifiedDefaultBlock } from '@wordpress/blocks'; /** * Internal dependencies */ +import EmptyBlockInserter from './empty-block-inserter'; import { InsertionPointOpenRef, default as InsertionPoint, } from './insertion-point'; -import SelectedBlockPopover from './selected-block-popover'; +import SelectedBlockTools from './selected-block-tools'; import { store as blockEditorStore } from '../../store'; import BlockContextualToolbar from './block-contextual-toolbar'; import usePopoverScroll from '../block-popover/use-popover-scroll'; import ZoomOutModeInserters from './zoom-out-mode-inserters'; function selector( select ) { - const { __unstableGetEditorMode, getSettings, isTyping } = - select( blockEditorStore ); + const { + getSelectedBlockClientId, + getFirstMultiSelectedBlockClientId, + getBlock, + getSettings, + __unstableGetEditorMode, + isTyping, + } = select( blockEditorStore ); + + const clientId = + getSelectedBlockClientId() || getFirstMultiSelectedBlockClientId(); + + const { name = '', attributes = {} } = getBlock( clientId ) || {}; return { - isZoomOutMode: __unstableGetEditorMode() === 'zoom-out', + clientId, hasFixedToolbar: getSettings().hasFixedToolbar, + hasSelectedBlock: clientId && name, isTyping: isTyping(), + isZoomOutMode: __unstableGetEditorMode() === 'zoom-out', + showEmptyBlockSideInserter: + clientId && + ! isTyping() && + __unstableGetEditorMode() === 'edit' && + isUnmodifiedDefaultBlock( { name, attributes } ), }; } @@ -46,10 +66,14 @@ export default function BlockTools( { ...props } ) { const isLargeViewport = useViewportMatch( 'medium' ); - const { hasFixedToolbar, isZoomOutMode, isTyping } = useSelect( - selector, - [] - ); + const { + clientId, + hasFixedToolbar, + hasSelectedBlock, + isTyping, + isZoomOutMode, + showEmptyBlockSideInserter, + } = useSelect( selector, [] ); const isMatch = useShortcutEventMatch(); const { getSelectedBlockClientIds, getBlockRootClientId } = useSelect( blockEditorStore ); @@ -64,6 +88,8 @@ export default function BlockTools( { moveBlocksDown, } = useDispatch( blockEditorStore ); + const selectedBlockToolsRef = useRef( null ); + function onKeyDown( event ) { if ( event.defaultPrevented ) return; @@ -106,6 +132,15 @@ export default function BlockTools( { insertBeforeBlock( clientIds[ 0 ] ); } } else if ( isMatch( 'core/block-editor/unselect', event ) ) { + if ( selectedBlockToolsRef?.current?.contains( event.target ) ) { + // This shouldn't be necessary, but we have a combination of a few things all combining to create a situation where: + // - Because the block toolbar uses createPortal to populate the block toolbar fills, we can't rely on the React event bubbling to hit the onKeyDown listener for the block toolbar + // - Since we can't use the React tree, we use the DOM tree which _should_ handle the event bubbling correctly from a `createPortal` element. + // - This bubbles via the React tree, which hits this `unselect` escape keypress before the block toolbar DOM event listener has access to it. + // An alternative would be to remove the addEventListener on the navigableToolbar and use this event to handle it directly right here. That feels hacky too though. + return; + } + const clientIds = getSelectedBlockClientIds(); if ( clientIds.length ) { event.preventDefault(); @@ -140,13 +175,28 @@ export default function BlockTools( { ) } { ! isZoomOutMode && ( hasFixedToolbar || ! isLargeViewport ) && ( - + ) } + + { showEmptyBlockSideInserter && ( + + ) } { /* Even if the toolbar is fixed, the block popover is still needed for navigation and zoom-out mode. */ } - + { ! showEmptyBlockSideInserter && hasSelectedBlock && ( + + ) } + { /* Used for the inline rich text toolbar. */ } { children } diff --git a/packages/block-editor/src/components/block-tools/selected-block-popover.js b/packages/block-editor/src/components/block-tools/selected-block-popover.js deleted file mode 100644 index 8c87d8ea3a473..0000000000000 --- a/packages/block-editor/src/components/block-tools/selected-block-popover.js +++ /dev/null @@ -1,265 +0,0 @@ -/** - * External dependencies - */ -import classnames from 'classnames'; - -/** - * WordPress dependencies - */ -import { useRef, useEffect } from '@wordpress/element'; -import { isUnmodifiedDefaultBlock } from '@wordpress/blocks'; -import { useDispatch, useSelect } from '@wordpress/data'; -import { useShortcut } from '@wordpress/keyboard-shortcuts'; - -/** - * Internal dependencies - */ -import BlockSelectionButton from './block-selection-button'; -import BlockContextualToolbar from './block-contextual-toolbar'; -import { store as blockEditorStore } from '../../store'; -import BlockPopover from '../block-popover'; -import useBlockToolbarPopoverProps from './use-block-toolbar-popover-props'; -import Inserter from '../inserter'; -import { useShouldContextualToolbarShow } from '../../utils/use-should-contextual-toolbar-show'; - -function selector( select ) { - const { - __unstableGetEditorMode, - hasMultiSelection, - isTyping, - getLastMultiSelectedBlockClientId, - } = select( blockEditorStore ); - - return { - editorMode: __unstableGetEditorMode(), - hasMultiSelection: hasMultiSelection(), - isTyping: isTyping(), - lastClientId: hasMultiSelection() - ? getLastMultiSelectedBlockClientId() - : null, - }; -} - -function SelectedBlockPopover( { - clientId, - rootClientId, - isEmptyDefaultBlock, - capturingClientId, - __unstablePopoverSlot, - __unstableContentRef, -} ) { - const { editorMode, hasMultiSelection, isTyping, lastClientId } = useSelect( - selector, - [] - ); - - const isInsertionPointVisible = useSelect( - ( select ) => { - const { - isBlockInsertionPointVisible, - getBlockInsertionPoint, - getBlockOrder, - } = select( blockEditorStore ); - - if ( ! isBlockInsertionPointVisible() ) { - return false; - } - - const insertionPoint = getBlockInsertionPoint(); - const order = getBlockOrder( insertionPoint.rootClientId ); - return order[ insertionPoint.index ] === clientId; - }, - [ clientId ] - ); - const isToolbarForced = useRef( false ); - const { shouldShowContextualToolbar, canFocusHiddenToolbar } = - useShouldContextualToolbarShow(); - - const { stopTyping } = useDispatch( blockEditorStore ); - - const showEmptyBlockSideInserter = - ! isTyping && editorMode === 'edit' && isEmptyDefaultBlock; - const shouldShowBreadcrumb = - ! hasMultiSelection && - ( editorMode === 'navigation' || editorMode === 'zoom-out' ); - - useShortcut( - 'core/block-editor/focus-toolbar', - () => { - isToolbarForced.current = true; - stopTyping( true ); - }, - { - isDisabled: ! canFocusHiddenToolbar, - } - ); - - useEffect( () => { - isToolbarForced.current = false; - } ); - - // Stores the active toolbar item index so the block toolbar can return focus - // to it when re-mounting. - const initialToolbarItemIndexRef = useRef(); - - useEffect( () => { - // Resets the index whenever the active block changes so this is not - // persisted. See https://github.com/WordPress/gutenberg/pull/25760#issuecomment-717906169 - initialToolbarItemIndexRef.current = undefined; - }, [ clientId ] ); - - const popoverProps = useBlockToolbarPopoverProps( { - contentElement: __unstableContentRef?.current, - clientId, - } ); - - if ( showEmptyBlockSideInserter ) { - return ( - -
- -
-
- ); - } - - if ( shouldShowBreadcrumb || shouldShowContextualToolbar ) { - return ( - - { shouldShowContextualToolbar && ( - { - initialToolbarItemIndexRef.current = index; - } } - // Resets the index whenever the active block changes so - // this is not persisted. See https://github.com/WordPress/gutenberg/pull/25760#issuecomment-717906169 - key={ clientId } - /> - ) } - { shouldShowBreadcrumb && ( - - ) } - - ); - } - - return null; -} - -function wrapperSelector( select ) { - const { - getSelectedBlockClientId, - getFirstMultiSelectedBlockClientId, - getBlockRootClientId, - getBlock, - getBlockParents, - __experimentalGetBlockListSettingsForBlocks, - } = select( blockEditorStore ); - - const clientId = - getSelectedBlockClientId() || getFirstMultiSelectedBlockClientId(); - - if ( ! clientId ) { - return; - } - - const { name, attributes = {} } = getBlock( clientId ) || {}; - const blockParentsClientIds = getBlockParents( clientId ); - - // Get Block List Settings for all ancestors of the current Block clientId. - const parentBlockListSettings = __experimentalGetBlockListSettingsForBlocks( - blockParentsClientIds - ); - - // Get the clientId of the topmost parent with the capture toolbars setting. - const capturingClientId = blockParentsClientIds.find( - ( parentClientId ) => - parentBlockListSettings[ parentClientId ] - ?.__experimentalCaptureToolbars - ); - - return { - clientId, - rootClientId: getBlockRootClientId( clientId ), - name, - isEmptyDefaultBlock: - name && isUnmodifiedDefaultBlock( { name, attributes } ), - capturingClientId, - }; -} - -export default function WrappedBlockPopover( { - __unstablePopoverSlot, - __unstableContentRef, -} ) { - const selected = useSelect( wrapperSelector, [] ); - - if ( ! selected ) { - return null; - } - - const { - clientId, - rootClientId, - name, - isEmptyDefaultBlock, - capturingClientId, - } = selected; - - if ( ! name ) { - return null; - } - - return ( - - ); -} diff --git a/packages/block-editor/src/components/block-tools/selected-block-tools.js b/packages/block-editor/src/components/block-tools/selected-block-tools.js new file mode 100644 index 0000000000000..31f042e891de2 --- /dev/null +++ b/packages/block-editor/src/components/block-tools/selected-block-tools.js @@ -0,0 +1,134 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + +/** + * WordPress dependencies + */ +import { forwardRef, useRef, useEffect } from '@wordpress/element'; +import { useDispatch, useSelect } from '@wordpress/data'; +import { useShortcut } from '@wordpress/keyboard-shortcuts'; + +/** + * Internal dependencies + */ +import BlockSelectionButton from './block-selection-button'; +import BlockContextualToolbar from './block-contextual-toolbar'; +import { store as blockEditorStore } from '../../store'; +import BlockPopover from '../block-popover'; +import useBlockToolbarPopoverProps from './use-block-toolbar-popover-props'; +import useSelectedBlockToolProps from './use-selected-block-tool-props'; +import { useShouldContextualToolbarShow } from '../../utils/use-should-contextual-toolbar-show'; + +function UnforwardedSelectedBlockTools( + { clientId, showEmptyBlockSideInserter, __unstableContentRef }, + ref +) { + const { + capturingClientId, + isInsertionPointVisible, + lastClientId, + rootClientId, + } = useSelectedBlockToolProps( clientId ); + + const { shouldShowBreadcrumb } = useSelect( ( select ) => { + const { hasMultiSelection, __unstableGetEditorMode } = + select( blockEditorStore ); + + const editorMode = __unstableGetEditorMode(); + + return { + shouldShowBreadcrumb: + ! hasMultiSelection() && + ( editorMode === 'navigation' || editorMode === 'zoom-out' ), + }; + }, [] ); + + const isToolbarForced = useRef( false ); + const { shouldShowContextualToolbar, canFocusHiddenToolbar } = + useShouldContextualToolbarShow(); + + const { stopTyping } = useDispatch( blockEditorStore ); + + useShortcut( + 'core/block-editor/focus-toolbar', + () => { + isToolbarForced.current = true; + stopTyping( true ); + }, + { + isDisabled: ! canFocusHiddenToolbar, + } + ); + + useEffect( () => { + isToolbarForced.current = false; + } ); + + // Stores the active toolbar item index so the block toolbar can return focus + // to it when re-mounting. + const initialToolbarItemIndexRef = useRef(); + + useEffect( () => { + // Resets the index whenever the active block changes so this is not + // persisted. See https://github.com/WordPress/gutenberg/pull/25760#issuecomment-717906169 + initialToolbarItemIndexRef.current = undefined; + }, [ clientId ] ); + + const popoverProps = useBlockToolbarPopoverProps( { + contentElement: __unstableContentRef?.current, + clientId, + } ); + + if ( showEmptyBlockSideInserter ) { + return null; + } + + if ( shouldShowBreadcrumb || shouldShowContextualToolbar ) { + return ( + + { shouldShowContextualToolbar && ( + { + initialToolbarItemIndexRef.current = index; + } } + // Resets the index whenever the active block changes so + // this is not persisted. See https://github.com/WordPress/gutenberg/pull/25760#issuecomment-717906169 + key={ clientId } + /> + ) } + { shouldShowBreadcrumb && ( + + ) } + + ); + } + + return null; +} + +export const SelectedBlockTools = forwardRef( UnforwardedSelectedBlockTools ); + +export default SelectedBlockTools; diff --git a/packages/block-editor/src/components/block-tools/use-selected-block-tool-props.js b/packages/block-editor/src/components/block-tools/use-selected-block-tool-props.js new file mode 100644 index 0000000000000..a2783f49bb538 --- /dev/null +++ b/packages/block-editor/src/components/block-tools/use-selected-block-tool-props.js @@ -0,0 +1,66 @@ +/** + * WordPress dependencies + */ +import { useSelect } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { store as blockEditorStore } from '../../store'; + +/** + * Returns props for the selected block tools and empty block inserter. + * + * @param {string} clientId Selected block client ID. + */ +export default function useSelectedBlockToolProps( clientId ) { + const selectedBlockProps = useSelect( + ( select ) => { + const { + getBlockRootClientId, + getBlockParents, + __experimentalGetBlockListSettingsForBlocks, + isBlockInsertionPointVisible, + getBlockInsertionPoint, + getBlockOrder, + hasMultiSelection, + getLastMultiSelectedBlockClientId, + } = select( blockEditorStore ); + + const blockParentsClientIds = getBlockParents( clientId ); + + // Get Block List Settings for all ancestors of the current Block clientId. + const parentBlockListSettings = + __experimentalGetBlockListSettingsForBlocks( + blockParentsClientIds + ); + + // Get the clientId of the topmost parent with the capture toolbars setting. + const capturingClientId = blockParentsClientIds.find( + ( parentClientId ) => + parentBlockListSettings[ parentClientId ] + ?.__experimentalCaptureToolbars + ); + + let isInsertionPointVisible = false; + if ( isBlockInsertionPointVisible() ) { + const insertionPoint = getBlockInsertionPoint(); + const order = getBlockOrder( insertionPoint.rootClientId ); + isInsertionPointVisible = + order[ insertionPoint.index ] === clientId; + } + + return { + capturingClientId, + isInsertionPointVisible, + lastClientId: hasMultiSelection() + ? getLastMultiSelectedBlockClientId() + : null, + rootClientId: getBlockRootClientId( clientId ), + }; + }, + [ clientId ] + ); + + return selectedBlockProps; +} diff --git a/packages/block-editor/src/components/global-styles/image-settings-panel.js b/packages/block-editor/src/components/global-styles/image-settings-panel.js index 0ae46605dc2d2..68054cf129e20 100644 --- a/packages/block-editor/src/components/global-styles/image-settings-panel.js +++ b/packages/block-editor/src/components/global-styles/image-settings-panel.js @@ -54,13 +54,13 @@ export default function ImageSettingsPanel( { // "RESET" button ONLY when the user has explicitly set a value in the // Global Styles. hasValue={ () => !! value?.lightbox } - label={ __( 'Expand on Click' ) } + label={ __( 'Expand on click' ) } onDeselect={ resetLightbox } isShownByDefault={ true } panelId={ panelId } > diff --git a/packages/block-editor/src/components/navigable-toolbar/index.js b/packages/block-editor/src/components/navigable-toolbar/index.js index 3e531c93c1198..e1204b90d0c5d 100644 --- a/packages/block-editor/src/components/navigable-toolbar/index.js +++ b/packages/block-editor/src/components/navigable-toolbar/index.js @@ -3,15 +3,23 @@ */ import { NavigableMenu, Toolbar } from '@wordpress/components'; import { + forwardRef, useState, useRef, useLayoutEffect, useEffect, useCallback, } from '@wordpress/element'; +import { useSelect } from '@wordpress/data'; import deprecated from '@wordpress/deprecated'; import { focus } from '@wordpress/dom'; import { useShortcut } from '@wordpress/keyboard-shortcuts'; +import { ESCAPE } from '@wordpress/keycodes'; + +/** + * Internal dependencies + */ +import { store as blockEditorStore } from '../../store'; function hasOnlyToolbarItem( elements ) { const dataProp = 'toolbarItem'; @@ -28,6 +36,7 @@ function hasFocusWithin( container ) { function focusFirstTabbableIn( container ) { const [ firstTabbable ] = focus.tabbable.find( container ); + if ( firstTabbable ) { firstTabbable.focus( { // When focusing newly mounted toolbars, @@ -38,7 +47,7 @@ function focusFirstTabbableIn( container ) { } } -function useIsAccessibleToolbar( ref ) { +function useIsAccessibleToolbar( toolbarRef ) { /* * By default, we'll assume the starting accessible state of the Toolbar * is true, as it seems to be the most common case. @@ -62,7 +71,7 @@ function useIsAccessibleToolbar( ref ) { ); const determineIsAccessibleToolbar = useCallback( () => { - const tabbables = focus.tabbable.find( ref.current ); + const tabbables = focus.tabbable.find( toolbarRef.current ); const onlyToolbarItem = hasOnlyToolbarItem( tabbables ); if ( ! onlyToolbarItem ) { deprecated( 'Using custom components as toolbar controls', { @@ -73,7 +82,7 @@ function useIsAccessibleToolbar( ref ) { } ); } setIsAccessibleToolbar( onlyToolbarItem ); - }, [] ); + }, [ toolbarRef ] ); useLayoutEffect( () => { // Toolbar buttons may be rendered asynchronously, so we use @@ -81,28 +90,32 @@ function useIsAccessibleToolbar( ref ) { const observer = new window.MutationObserver( determineIsAccessibleToolbar ); - observer.observe( ref.current, { childList: true, subtree: true } ); + observer.observe( toolbarRef.current, { + childList: true, + subtree: true, + } ); return () => observer.disconnect(); - }, [ isAccessibleToolbar ] ); + }, [ determineIsAccessibleToolbar, isAccessibleToolbar, toolbarRef ] ); return isAccessibleToolbar; } -function useToolbarFocus( - ref, +function useToolbarFocus( { + toolbarRef, focusOnMount, isAccessibleToolbar, defaultIndex, onIndexChange, - shouldUseKeyboardFocusShortcut -) { + shouldUseKeyboardFocusShortcut, + focusEditorOnEscape, +} ) { // Make sure we don't use modified versions of this prop. const [ initialFocusOnMount ] = useState( focusOnMount ); const [ initialIndex ] = useState( defaultIndex ); const focusToolbar = useCallback( () => { - focusFirstTabbableIn( ref.current ); - }, [] ); + focusFirstTabbableIn( toolbarRef.current ); + }, [ toolbarRef ] ); const focusToolbarViaShortcut = () => { if ( shouldUseKeyboardFocusShortcut ) { @@ -121,7 +134,7 @@ function useToolbarFocus( useEffect( () => { // Store ref so we have access on useEffect cleanup: https://legacy.reactjs.org/blog/2020/08/10/react-v17-rc.html#effect-cleanup-timing - const navigableToolbarRef = ref.current; + const navigableToolbarRef = toolbarRef.current; // If initialIndex is passed, we focus on that toolbar item when the // toolbar gets mounted and initial focus is not forced. // We have to wait for the next browser paint because block controls aren't @@ -150,32 +163,73 @@ function useToolbarFocus( const index = items.findIndex( ( item ) => item.tabIndex === 0 ); onIndexChange( index ); }; - }, [ initialIndex, initialFocusOnMount ] ); + }, [ initialIndex, initialFocusOnMount, toolbarRef ] ); + + const { lastFocus } = useSelect( ( select ) => { + const { getLastFocus } = select( blockEditorStore ); + return { + lastFocus: getLastFocus(), + }; + }, [] ); + /** + * Handles returning focus to the block editor canvas when pressing escape. + */ + useEffect( () => { + const navigableToolbarRef = toolbarRef.current; + + if ( focusEditorOnEscape ) { + const handleKeyDown = ( event ) => { + if ( event.keyCode === ESCAPE && lastFocus?.current ) { + // Focus the last focused element when pressing escape. + event.preventDefault(); + lastFocus.current.focus(); + } + }; + navigableToolbarRef.addEventListener( 'keydown', handleKeyDown ); + return () => { + navigableToolbarRef.removeEventListener( + 'keydown', + handleKeyDown + ); + }; + } + }, [ focusEditorOnEscape, lastFocus, toolbarRef ] ); } -function NavigableToolbar( { - children, - focusOnMount, - shouldUseKeyboardFocusShortcut = true, - __experimentalInitialIndex: initialIndex, - __experimentalOnIndexChange: onIndexChange, - ...props -} ) { - const ref = useRef(); - const isAccessibleToolbar = useIsAccessibleToolbar( ref ); +function UnforwardedNavigableToolbar( + { + children, + focusOnMount, + focusEditorOnEscape = false, + shouldUseKeyboardFocusShortcut = true, + __experimentalInitialIndex: initialIndex, + __experimentalOnIndexChange: onIndexChange, + ...props + }, + ref +) { + const maybeRef = useRef(); + // If a ref was not forwarded, we create one. + const toolbarRef = ref || maybeRef; + const isAccessibleToolbar = useIsAccessibleToolbar( toolbarRef ); - useToolbarFocus( - ref, + useToolbarFocus( { + toolbarRef, focusOnMount, isAccessibleToolbar, - initialIndex, + defaultIndex: initialIndex, onIndexChange, - shouldUseKeyboardFocusShortcut - ); + shouldUseKeyboardFocusShortcut, + focusEditorOnEscape, + } ); if ( isAccessibleToolbar ) { return ( - + { children } ); @@ -185,7 +239,7 @@ function NavigableToolbar( { { children } @@ -193,4 +247,6 @@ function NavigableToolbar( { ); } +export const NavigableToolbar = forwardRef( UnforwardedNavigableToolbar ); + export default NavigableToolbar; diff --git a/packages/block-editor/src/components/plain-text/README.md b/packages/block-editor/src/components/plain-text/README.md index 25b18fcddc48f..4e59789fd612c 100644 --- a/packages/block-editor/src/components/plain-text/README.md +++ b/packages/block-editor/src/components/plain-text/README.md @@ -34,7 +34,7 @@ wp.blocks.registerBlockType( /* ... */, { }, edit: function( props ) { - return React.createElement( wp.editor.PlainText, { + return React.createElement( wp.blockEditor.PlainText, { className: props.className, value: props.attributes.content, onChange: function( content ) { @@ -48,8 +48,8 @@ wp.blocks.registerBlockType( /* ... */, { {% ESNext %} ```js -const { registerBlockType } = wp.blocks; -const { PlainText } = wp.editor; +import { registerBlockType } from '@wordpress/blocks'; +import { PlainText } from '@wordpress/block-editor'; registerBlockType( /* ... */, { // ... diff --git a/packages/block-editor/src/components/resizable-box-popover/index.js b/packages/block-editor/src/components/resizable-box-popover/index.js index c2400dbe07f91..12a61aceaaf38 100644 --- a/packages/block-editor/src/components/resizable-box-popover/index.js +++ b/packages/block-editor/src/components/resizable-box-popover/index.js @@ -17,7 +17,7 @@ export default function ResizableBoxPopover( { diff --git a/packages/block-editor/src/components/rich-text/README.md b/packages/block-editor/src/components/rich-text/README.md index eea63d0b2912d..fba07d7c5d852 100644 --- a/packages/block-editor/src/components/rich-text/README.md +++ b/packages/block-editor/src/components/rich-text/README.md @@ -94,7 +94,7 @@ wp.blocks.registerBlockType( /* ... */, { }, edit: function( props ) { - return React.createElement( wp.editor.RichText, { + return React.createElement( wp.blockEditor.RichText, { tagName: 'h2', className: props.className, value: props.attributes.content, @@ -105,7 +105,7 @@ wp.blocks.registerBlockType( /* ... */, { }, save: function( props ) { - return React.createElement( wp.editor.RichText.Content, { + return React.createElement( wp.blockEditor.RichText.Content, { tagName: 'h2', value: props.attributes.content } ); } @@ -115,8 +115,8 @@ wp.blocks.registerBlockType( /* ... */, { {% ESNext %} ```js -const { registerBlockType } = wp.blocks; -const { RichText } = wp.editor; +import { registerBlockType } from '@wordpress/blocks'; +import { RichText } from '@wordpress/block-editor'; registerBlockType( /* ... */, { // ... @@ -161,7 +161,7 @@ wp.richText.registerFormatType( /* ... */, { /* ... */ edit: function( props ) { return React.createElement( - wp.editor.RichTextToolbarButton, { + wp.blockEditor.RichTextToolbarButton, { icon: 'editor-code', title: 'My formatting button', onClick: function() { /* ... */ } @@ -175,8 +175,8 @@ wp.richText.registerFormatType( /* ... */, { {% ESNext %} ```js -import { registerFormatType } from 'wp-rich-text'; -import { richTextToolbarButton } from 'wp-editor'; +import { registerFormatType } from '@wordpress/rich-text'; +import { RichTextToolbarButton } from '@wordpress/block-editor'; registerFormatType( /* ... */, { /* ... */ diff --git a/packages/block-editor/src/components/writing-flow/use-tab-nav.js b/packages/block-editor/src/components/writing-flow/use-tab-nav.js index 616da1bc75813..b1fb1800a53ea 100644 --- a/packages/block-editor/src/components/writing-flow/use-tab-nav.js +++ b/packages/block-editor/src/components/writing-flow/use-tab-nav.js @@ -17,15 +17,20 @@ export default function useTabNav() { const container = useRef(); const focusCaptureBeforeRef = useRef(); const focusCaptureAfterRef = useRef(); - const lastFocus = useRef(); + const { hasMultiSelection, getSelectedBlockClientId, getBlockCount } = useSelect( blockEditorStore ); - const { setNavigationMode } = useDispatch( blockEditorStore ); + const { setNavigationMode, setLastFocus } = useDispatch( blockEditorStore ); const isNavigationMode = useSelect( ( select ) => select( blockEditorStore ).isNavigationMode(), [] ); + const lastFocus = useSelect( + ( select ) => select( blockEditorStore ).getLastFocus(), + [] + ); + // Don't allow tabbing to this element in Navigation mode. const focusCaptureTabIndex = ! isNavigationMode ? '0' : undefined; @@ -158,7 +163,7 @@ export default function useTabNav() { } function onFocusOut( event ) { - lastFocus.current = event.target; + setLastFocus( { ...lastFocus, current: event.target } ); const { ownerDocument } = node; diff --git a/packages/block-editor/src/hooks/align.js b/packages/block-editor/src/hooks/align.js index 8f1284eedb258..563c7bae6cde9 100644 --- a/packages/block-editor/src/hooks/align.js +++ b/packages/block-editor/src/hooks/align.js @@ -98,8 +98,8 @@ export function addAttribute( settings ) { ...settings.attributes, align: { type: 'string', - // Allow for '' since it is used by updateAlignment function - // in withToolbarControls for special cases with defined default values. + // Allow for '' since it is used by the `updateAlignment` function + // in toolbar controls for special cases with defined default values. enum: [ ...ALL_ALIGNMENTS, '' ], }, }; @@ -108,6 +108,50 @@ export function addAttribute( settings ) { return settings; } +function BlockEditAlignmentToolbarControls( { + blockName, + attributes, + setAttributes, +} ) { + // Compute the block valid alignments by taking into account, + // if the theme supports wide alignments or not and the layout's + // available alignments. We do that for conditionally rendering + // Slot. + const blockAllowedAlignments = getValidAlignments( + getBlockSupport( blockName, 'align' ), + hasBlockSupport( blockName, 'alignWide', true ) + ); + + const validAlignments = useAvailableAlignments( + blockAllowedAlignments + ).map( ( { name } ) => name ); + const blockEditingMode = useBlockEditingMode(); + if ( ! validAlignments.length || blockEditingMode !== 'default' ) { + return null; + } + + const updateAlignment = ( nextAlign ) => { + if ( ! nextAlign ) { + const blockType = getBlockType( blockName ); + const blockDefaultAlign = blockType?.attributes?.align?.default; + if ( blockDefaultAlign ) { + nextAlign = ''; + } + } + setAttributes( { align: nextAlign } ); + }; + + return ( + + + + ); +} + /** * Override the default edit UI to include new toolbar controls for block * alignment, if block defines support. @@ -116,52 +160,28 @@ export function addAttribute( settings ) { * * @return {Function} Wrapped component. */ -export const withToolbarControls = createHigherOrderComponent( +export const withAlignmentControls = createHigherOrderComponent( ( BlockEdit ) => ( props ) => { - const blockEdit = ; - const { name: blockName } = props; - // Compute the block valid alignments by taking into account, - // if the theme supports wide alignments or not and the layout's - // availble alignments. We do that for conditionally rendering - // Slot. - const blockAllowedAlignments = getValidAlignments( - getBlockSupport( blockName, 'align' ), - hasBlockSupport( blockName, 'alignWide', true ) + const hasAlignmentSupport = hasBlockSupport( + props.name, + 'align', + false ); - const validAlignments = useAvailableAlignments( - blockAllowedAlignments - ).map( ( { name } ) => name ); - const blockEditingMode = useBlockEditingMode(); - if ( ! validAlignments.length || blockEditingMode !== 'default' ) { - return blockEdit; - } - - const updateAlignment = ( nextAlign ) => { - if ( ! nextAlign ) { - const blockType = getBlockType( props.name ); - const blockDefaultAlign = blockType?.attributes?.align?.default; - if ( blockDefaultAlign ) { - nextAlign = ''; - } - } - props.setAttributes( { align: nextAlign } ); - }; - return ( <> - - - - { blockEdit } + ) } + ); }, - 'withToolbarControls' + 'withAlignmentControls' ); function BlockListBlockWithDataAlign( { block: BlockListBlock, props } ) { @@ -237,7 +257,7 @@ export function addAssignedAlign( props, blockType, attributes ) { addFilter( 'blocks.registerBlockType', - 'core/align/addAttribute', + 'core/editor/align/addAttribute', addAttribute ); addFilter( @@ -248,10 +268,10 @@ addFilter( addFilter( 'editor.BlockEdit', 'core/editor/align/with-toolbar-controls', - withToolbarControls + withAlignmentControls ); addFilter( 'blocks.getSaveContent.extraProps', - 'core/align/addAssignedAlign', + 'core/editor/align/addAssignedAlign', addAssignedAlign ); diff --git a/packages/block-editor/src/hooks/align.native.js b/packages/block-editor/src/hooks/align.native.js index 75b25b8dca5a6..1bf375b654ad4 100644 --- a/packages/block-editor/src/hooks/align.native.js +++ b/packages/block-editor/src/hooks/align.native.js @@ -36,8 +36,8 @@ addFilter( ...settings.attributes, align: { type: 'string', - // Allow for '' since it is used by updateAlignment function - // in withToolbarControls for special cases with defined default values. + // Allow for '' since it is used by the `updateAlignment` function + // in toolbar controls for special cases with defined default values. enum: [ ...ALIGNMENTS, '' ], }, }; diff --git a/packages/block-editor/src/hooks/anchor.js b/packages/block-editor/src/hooks/anchor.js index 6e8acd5eb7f2e..7a8f507d9674e 100644 --- a/packages/block-editor/src/hooks/anchor.js +++ b/packages/block-editor/src/hooks/anchor.js @@ -52,6 +52,69 @@ export function addAttribute( settings ) { return settings; } +function BlockEditAnchorControl( { blockName, attributes, setAttributes } ) { + const blockEditingMode = useBlockEditingMode(); + + const isWeb = Platform.OS === 'web'; + const textControl = ( + + { __( + 'Enter a word or two — without spaces — to make a unique web address just for this block, called an “anchor.” Then, you’ll be able to link directly to this section of your page.' + ) } + + { isWeb && ( + + { __( 'Learn more about anchors' ) } + + ) } + + } + value={ attributes.anchor || '' } + placeholder={ ! isWeb ? __( 'Add an anchor' ) : null } + onChange={ ( nextValue ) => { + nextValue = nextValue.replace( ANCHOR_REGEX, '-' ); + setAttributes( { + anchor: nextValue, + } ); + } } + autoCapitalize="none" + autoComplete="off" + /> + ); + + return ( + <> + { isWeb && blockEditingMode === 'default' && ( + + { textControl } + + ) } + { /* + * We plan to remove scoping anchors to 'core/heading' to support + * anchors for all eligble blocks. Additionally we plan to explore + * leveraging InspectorAdvancedControls instead of a custom + * PanelBody title. https://github.com/WordPress/gutenberg/issues/28363 + */ } + { ! isWeb && blockName === 'core/heading' && ( + + + { textControl } + + + ) } + + ); +} + /** * Override the default edit UI to include a new block inspector control for * assigning the anchor ID, if block supports anchor. @@ -60,79 +123,23 @@ export function addAttribute( settings ) { * * @return {Component} Wrapped component. */ -export const withInspectorControl = createHigherOrderComponent( - ( BlockEdit ) => { - return ( props ) => { - const hasAnchor = hasBlockSupport( props.name, 'anchor' ); - const blockEditingMode = useBlockEditingMode(); - - if ( hasAnchor && props.isSelected ) { - const isWeb = Platform.OS === 'web'; - const textControl = ( - - { __( - 'Enter a word or two — without spaces — to make a unique web address just for this block, called an “anchor.” Then, you’ll be able to link directly to this section of your page.' - ) } - - { isWeb && ( - - { __( 'Learn more about anchors' ) } - - ) } - - } - value={ props.attributes.anchor || '' } - placeholder={ ! isWeb ? __( 'Add an anchor' ) : null } - onChange={ ( nextValue ) => { - nextValue = nextValue.replace( ANCHOR_REGEX, '-' ); - props.setAttributes( { - anchor: nextValue, - } ); - } } - autoCapitalize="none" - autoComplete="off" - /> - ); - - return ( - <> - - { isWeb && blockEditingMode === 'default' && ( - - { textControl } - - ) } - { /* - * We plan to remove scoping anchors to 'core/heading' to support - * anchors for all eligble blocks. Additionally we plan to explore - * leveraging InspectorAdvancedControls instead of a custom - * PanelBody title. https://github.com/WordPress/gutenberg/issues/28363 - */ } - { ! isWeb && props.name === 'core/heading' && ( - - - { textControl } - - - ) } - - ); - } - - return ; - }; - }, - 'withInspectorControl' -); +export const withAnchorControls = createHigherOrderComponent( ( BlockEdit ) => { + return ( props ) => { + return ( + <> + + { props.isSelected && + hasBlockSupport( props.name, 'anchor' ) && ( + + ) } + + ); + }; +}, 'withAnchorControls' ); /** * Override props assigned to save component to inject anchor ID, if block @@ -156,11 +163,11 @@ export function addSaveProps( extraProps, blockType, attributes ) { addFilter( 'blocks.registerBlockType', 'core/anchor/attribute', addAttribute ); addFilter( 'editor.BlockEdit', - 'core/editor/anchor/with-inspector-control', - withInspectorControl + 'core/editor/anchor/with-inspector-controls', + withAnchorControls ); addFilter( 'blocks.getSaveContent.extraProps', - 'core/anchor/save-props', + 'core/editor/anchor/save-props', addSaveProps ); diff --git a/packages/block-editor/src/hooks/block-hooks.js b/packages/block-editor/src/hooks/block-hooks.js index 3285854db6c79..fc08090b49889 100644 --- a/packages/block-editor/src/hooks/block-hooks.js +++ b/packages/block-editor/src/hooks/block-hooks.js @@ -235,23 +235,26 @@ function BlockHooksControl( props ) { ); } -export const withBlockHooks = createHigherOrderComponent( ( BlockEdit ) => { - return ( props ) => { - const blockEdit = ; - return ( - <> - { blockEdit } - - - ); - }; -}, 'withBlockHooks' ); +export const withBlockHooksControls = createHigherOrderComponent( + ( BlockEdit ) => { + return ( props ) => { + const blockEdit = ; + return ( + <> + { blockEdit } + + + ); + }; + }, + 'withBlockHooksControls' +); addFilter( 'editor.BlockEdit', - 'core/block-hooks/with-inspector-control', - withBlockHooks + 'core/editor/block-hooks/with-inspector-controls', + withBlockHooksControls ); diff --git a/packages/block-editor/src/hooks/block-rename-ui.js b/packages/block-editor/src/hooks/block-rename-ui.js index f1e488eca03a3..6af17da6fad7f 100644 --- a/packages/block-editor/src/hooks/block-rename-ui.js +++ b/packages/block-editor/src/hooks/block-rename-ui.js @@ -187,7 +187,7 @@ function BlockRenameControl( props ) { ); } -export const withBlockRenameControl = createHigherOrderComponent( +export const withBlockRenameControls = createHigherOrderComponent( ( BlockEdit ) => ( props ) => { const { clientId, name, attributes, setAttributes, isSelected } = props; @@ -216,11 +216,11 @@ export const withBlockRenameControl = createHigherOrderComponent( ); }, - 'withToolbarControls' + 'withBlockRenameControls' ); addFilter( 'editor.BlockEdit', - 'core/block-rename-ui/with-block-rename-control', - withBlockRenameControl + 'core/block-rename-ui/with-block-rename-controls', + withBlockRenameControls ); diff --git a/packages/block-editor/src/hooks/content-lock-ui.js b/packages/block-editor/src/hooks/content-lock-ui.js index c1cf7016bfd44..5d277d6a516d2 100644 --- a/packages/block-editor/src/hooks/content-lock-ui.js +++ b/packages/block-editor/src/hooks/content-lock-ui.js @@ -37,7 +37,7 @@ function StopEditingAsBlocksOnOutsideSelect( { return null; } -export const withBlockControls = createHigherOrderComponent( +export const withContentLockControls = createHigherOrderComponent( ( BlockEdit ) => ( props ) => { const { getBlockListSettings, getSettings } = useSelect( blockEditorStore ); @@ -155,11 +155,11 @@ export const withBlockControls = createHigherOrderComponent( ); }, - 'withToolbarControls' + 'withContentLockControls' ); addFilter( 'editor.BlockEdit', 'core/content-lock-ui/with-block-controls', - withBlockControls + withContentLockControls ); diff --git a/packages/block-editor/src/hooks/custom-class-name.js b/packages/block-editor/src/hooks/custom-class-name.js index dd4f4cd8c51e2..6336d64421f57 100644 --- a/packages/block-editor/src/hooks/custom-class-name.js +++ b/packages/block-editor/src/hooks/custom-class-name.js @@ -72,7 +72,7 @@ function CustomClassNameControls( { attributes, setAttributes } ) { * * @return {Component} Wrapped component. */ -export const withInspectorControl = createHigherOrderComponent( +export const withCustomClassNameControls = createHigherOrderComponent( ( BlockEdit ) => { return ( props ) => { const hasCustomClassName = hasBlockSupport( @@ -94,7 +94,7 @@ export const withInspectorControl = createHigherOrderComponent( ); }; }, - 'withInspectorControl' + 'withCustomClassNameControls' ); /** @@ -163,17 +163,17 @@ export function addTransforms( result, source, index, results ) { addFilter( 'blocks.registerBlockType', - 'core/custom-class-name/attribute', + 'core/editor/custom-class-name/attribute', addAttribute ); addFilter( 'editor.BlockEdit', - 'core/editor/custom-class-name/with-inspector-control', - withInspectorControl + 'core/editor/custom-class-name/with-inspector-controls', + withCustomClassNameControls ); addFilter( 'blocks.getSaveContent.extraProps', - 'core/custom-class-name/save-props', + 'core/editor/custom-class-name/save-props', addSaveProps ); diff --git a/packages/block-editor/src/hooks/custom-fields.js b/packages/block-editor/src/hooks/custom-fields.js index 9affb55c3ea71..31721569781b6 100644 --- a/packages/block-editor/src/hooks/custom-fields.js +++ b/packages/block-editor/src/hooks/custom-fields.js @@ -44,7 +44,7 @@ function addAttribute( settings ) { * * @return {Component} Wrapped component. */ -const withInspectorControl = createHigherOrderComponent( ( BlockEdit ) => { +const withCustomFieldsControls = createHigherOrderComponent( ( BlockEdit ) => { return ( props ) => { const blockEditingMode = useBlockEditingMode(); const hasCustomFieldsSupport = hasBlockSupport( @@ -123,17 +123,17 @@ const withInspectorControl = createHigherOrderComponent( ( BlockEdit ) => { return ; }; -}, 'withInspectorControl' ); +}, 'withCustomFieldsControls' ); if ( window.__experimentalConnections ) { addFilter( 'blocks.registerBlockType', - 'core/connections/attribute', + 'core/editor/connections/attribute', addAttribute ); addFilter( 'editor.BlockEdit', - 'core/connections/with-inspector-control', - withInspectorControl + 'core/editor/connections/with-inspector-controls', + withCustomFieldsControls ); } diff --git a/packages/block-editor/src/hooks/duotone.js b/packages/block-editor/src/hooks/duotone.js index 1c46246d10641..5470ea5789f3a 100644 --- a/packages/block-editor/src/hooks/duotone.js +++ b/packages/block-editor/src/hooks/duotone.js @@ -102,6 +102,7 @@ function DuotonePanel( { attributes, setAttributes, name } ) { const style = attributes?.style; const duotoneStyle = style?.color?.duotone; const settings = useBlockSettings( name ); + const blockEditingMode = useBlockEditingMode(); const duotonePalette = useMultiOriginPresets( { presetSetting: 'color.duotone', @@ -124,6 +125,10 @@ function DuotonePanel( { attributes, setAttributes, name } ) { return null; } + if ( blockEditingMode !== 'default' ) { + return null; + } + const duotonePresetOrColors = ! Array.isArray( duotoneStyle ) ? getColorsFromDuotonePreset( duotoneStyle, duotonePalette ) : duotoneStyle; @@ -219,17 +224,13 @@ const withDuotoneControls = createHigherOrderComponent( 'filter.duotone' ); - const blockEditingMode = useBlockEditingMode(); - // CAUTION: code added before this line will be executed // for all blocks, not just those that support duotone. Code added // above this line should be carefully evaluated for its impact on // performance. return ( <> - { hasDuotoneSupport && blockEditingMode === 'default' && ( - - ) } + { hasDuotoneSupport && } ); diff --git a/packages/block-editor/src/hooks/layout.js b/packages/block-editor/src/hooks/layout.js index 95fc90fc0446f..c8e81d3828392 100644 --- a/packages/block-editor/src/hooks/layout.js +++ b/packages/block-editor/src/hooks/layout.js @@ -151,6 +151,10 @@ function LayoutPanel( { setAttributes, attributes, name: blockName } ) { }, [] ); const blockEditingMode = useBlockEditingMode(); + if ( blockEditingMode !== 'default' ) { + return null; + } + const layoutBlockSupport = getBlockSupport( blockName, layoutBlockSupportKey, @@ -270,7 +274,7 @@ function LayoutPanel( { setAttributes, attributes, name: blockName } ) { ) } - { ! inherit && blockEditingMode === 'default' && layoutType && ( + { ! inherit && layoutType && ( ( props ) => { - const { name: blockName } = props; - const supportLayout = hasLayoutBlockSupport( blockName ); + const supportLayout = hasLayoutBlockSupport( props.name ); - const blockEditingMode = useBlockEditingMode(); return [ - supportLayout && blockEditingMode === 'default' && ( - - ), + supportLayout && , , ]; }, - 'withInspectorControls' + 'withLayoutControls' ); /** @@ -501,5 +501,5 @@ addFilter( addFilter( 'editor.BlockEdit', 'core/editor/layout/with-inspector-controls', - withInspectorControls + withLayoutControls ); diff --git a/packages/block-editor/src/hooks/position.js b/packages/block-editor/src/hooks/position.js index d040a2c39d21d..bdd8095e9b250 100644 --- a/packages/block-editor/src/hooks/position.js +++ b/packages/block-editor/src/hooks/position.js @@ -329,7 +329,7 @@ export function PositionPanel( props ) { * * @return {Function} Wrapped component. */ -export const withInspectorControls = createHigherOrderComponent( +export const withPositionControls = createHigherOrderComponent( ( BlockEdit ) => ( props ) => { const { name: blockName } = props; const positionSupport = hasBlockSupport( @@ -346,7 +346,7 @@ export const withInspectorControls = createHigherOrderComponent( , ]; }, - 'withInspectorControls' + 'withPositionControls' ); /** @@ -413,5 +413,5 @@ addFilter( addFilter( 'editor.BlockEdit', 'core/editor/position/with-inspector-controls', - withInspectorControls + withPositionControls ); diff --git a/packages/block-editor/src/hooks/style.js b/packages/block-editor/src/hooks/style.js index 9c95f0a12996e..356042e3017ca 100644 --- a/packages/block-editor/src/hooks/style.js +++ b/packages/block-editor/src/hooks/style.js @@ -354,7 +354,7 @@ export function addEditProps( settings ) { * * @return {Function} Wrapped component. */ -export const withBlockControls = createHigherOrderComponent( +export const withBlockStyleControls = createHigherOrderComponent( ( BlockEdit ) => ( props ) => { if ( ! hasStyleSupport( props.name ) ) { return ; @@ -378,7 +378,7 @@ export const withBlockControls = createHigherOrderComponent( ); }, - 'withToolbarControls' + 'withBlockStyleControls' ); // Defines which element types are supported, including their hover styles or @@ -537,7 +537,7 @@ addFilter( addFilter( 'editor.BlockEdit', 'core/style/with-block-controls', - withBlockControls + withBlockStyleControls ); addFilter( diff --git a/packages/block-editor/src/hooks/test/align.js b/packages/block-editor/src/hooks/test/align.js index d6a9b3aba65ce..b928e6eafc8b2 100644 --- a/packages/block-editor/src/hooks/test/align.js +++ b/packages/block-editor/src/hooks/test/align.js @@ -22,7 +22,7 @@ import BlockEdit from '../../components/block-edit'; import BlockEditorProvider from '../../components/provider'; import { getValidAlignments, - withToolbarControls, + withAlignmentControls, withDataAlign, addAssignedAlign, } from '../align'; @@ -157,7 +157,7 @@ describe( 'align', () => { } ); } ); - describe( 'withToolbarControls', () => { + describe( 'withAlignControls', () => { const componentProps = { name: 'core/foo', attributes: {}, @@ -167,7 +167,7 @@ describe( 'align', () => { it( 'should do nothing if no valid alignments', () => { registerBlockType( 'core/foo', blockSettings ); - const EnhancedComponent = withToolbarControls( + const EnhancedComponent = withAlignmentControls( ( { wrapperProps } ) =>
); @@ -197,7 +197,7 @@ describe( 'align', () => { }, } ); - const EnhancedComponent = withToolbarControls( + const EnhancedComponent = withAlignmentControls( ( { wrapperProps } ) =>
); diff --git a/packages/block-editor/src/store/actions.js b/packages/block-editor/src/store/actions.js index 2975a41dbb9d9..4f6ce7b5b044c 100644 --- a/packages/block-editor/src/store/actions.js +++ b/packages/block-editor/src/store/actions.js @@ -1980,3 +1980,18 @@ export function unsetBlockEditingMode( clientId = '' ) { clientId, }; } + +/** + * Action that sets the element that had focus when focus leaves the editor canvas. + * + * @param {Object} lastFocus The last focused element. + * + * + * @return {Object} Action object. + */ +export function setLastFocus( lastFocus = null ) { + return { + type: 'LAST_FOCUS', + lastFocus, + }; +} diff --git a/packages/block-editor/src/store/reducer.js b/packages/block-editor/src/store/reducer.js index 4373182d98662..d5ff85e9e4257 100644 --- a/packages/block-editor/src/store/reducer.js +++ b/packages/block-editor/src/store/reducer.js @@ -1962,6 +1962,24 @@ export function registeredInserterMediaCategories( state = [], action ) { case 'REGISTER_INSERTER_MEDIA_CATEGORY': return [ ...state, action.category ]; } + + return state; +} + +/** + * Reducer setting last focused element + * + * @param {boolean} state Current state. + * @param {Object} action Dispatched action. + * + * @return {boolean} Updated state. + */ +export function lastFocus( state = false, action ) { + switch ( action.type ) { + case 'LAST_FOCUS': + return action.lastFocus; + } + return state; } @@ -1981,6 +1999,7 @@ const combinedReducers = combineReducers( { settings, preferences, lastBlockAttributesChange, + lastFocus, editorMode, hasBlockMovingClientId, highlightedBlock, diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index cb1f8ef49809d..e9d17d86a2672 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -3022,3 +3022,14 @@ export const isGroupable = createRegistrySelector( ); } ); + +/** + * Returns the element of the last element that had focus when focus left the editor canvas. + * + * @param {Object} state Block editor state. + * + * @return {Object} Element. + */ +export function getLastFocus( state ) { + return state.lastFocus; +} diff --git a/packages/block-library/CHANGELOG.md b/packages/block-library/CHANGELOG.md index 2a38217d9b1b0..22f02cfcd6288 100644 --- a/packages/block-library/CHANGELOG.md +++ b/packages/block-library/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 8.22.0 (2023-11-02) + ## 8.21.0 (2023-10-18) ### Bug Fix diff --git a/packages/block-library/package.json b/packages/block-library/package.json index 2eeddc3105dc8..5fdec495443ef 100644 --- a/packages/block-library/package.json +++ b/packages/block-library/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/block-library", - "version": "8.21.0", + "version": "8.22.0", "description": "Block library for the WordPress editor.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/block-library/src/block/edit.js b/packages/block-library/src/block/edit.js index 89997d2c066c8..293f09640569d 100644 --- a/packages/block-library/src/block/edit.js +++ b/packages/block-library/src/block/edit.js @@ -35,6 +35,7 @@ import { useRef, useMemo } from '@wordpress/element'; */ import { unlock } from '../lock-unlock'; +const { useLayoutClasses } = unlock( blockEditorPrivateApis ); const fullAlignments = [ 'full', 'wide', 'left', 'right' ]; const useInferredLayout = ( blocks, parentLayout ) => { @@ -71,7 +72,6 @@ export default function ReusableBlockEdit( { attributes: { ref }, __unstableParentLayout: parentLayout, } ) { - const { useLayoutClasses } = unlock( blockEditorPrivateApis ); const hasAlreadyRendered = useHasRecursion( ref ); const { record, hasResolved } = useEntityRecord( 'postType', diff --git a/packages/block-library/src/form-input/block.json b/packages/block-library/src/form-input/block.json index f166d2f6754f6..067b7ac69430c 100644 --- a/packages/block-library/src/form-input/block.json +++ b/packages/block-library/src/form-input/block.json @@ -3,9 +3,9 @@ "apiVersion": 3, "__experimental": true, "name": "core/form-input", - "title": "Input field", + "title": "Input Field", "category": "common", - "parent": [ "core/form" ], + "ancestor": [ "core/form" ], "description": "The basic building block for forms.", "keywords": [ "input", "form" ], "textdomain": "default", diff --git a/packages/block-library/src/form-input/variations.js b/packages/block-library/src/form-input/variations.js index cc205feb89501..322043d5e2701 100644 --- a/packages/block-library/src/form-input/variations.js +++ b/packages/block-library/src/form-input/variations.js @@ -6,7 +6,7 @@ import { __ } from '@wordpress/i18n'; const variations = [ { name: 'text', - title: __( 'Text input' ), + title: __( 'Text Input' ), icon: 'edit-page', description: __( 'A generic text input.' ), attributes: { type: 'text' }, @@ -17,7 +17,7 @@ const variations = [ }, { name: 'textarea', - title: __( 'Textarea input' ), + title: __( 'Textarea Input' ), icon: 'testimonial', description: __( 'A textarea input to allow entering multiple lines of text.' @@ -29,7 +29,7 @@ const variations = [ }, { name: 'checkbox', - title: __( 'Checkbox input' ), + title: __( 'Checkbox Input' ), description: __( 'A simple checkbox input.' ), icon: 'forms', attributes: { type: 'checkbox', inlineLabel: true }, @@ -39,7 +39,7 @@ const variations = [ }, { name: 'email', - title: __( 'Email input' ), + title: __( 'Email Input' ), icon: 'email', description: __( 'Used for email addresses.' ), attributes: { type: 'email' }, @@ -49,7 +49,7 @@ const variations = [ }, { name: 'url', - title: __( 'URL input' ), + title: __( 'URL Input' ), icon: 'admin-site', description: __( 'Used for URLs.' ), attributes: { type: 'url' }, @@ -59,7 +59,7 @@ const variations = [ }, { name: 'tel', - title: __( 'Telephone input' ), + title: __( 'Telephone Input' ), icon: 'phone', description: __( 'Used for phone numbers.' ), attributes: { type: 'tel' }, @@ -69,7 +69,7 @@ const variations = [ }, { name: 'number', - title: __( 'Number input' ), + title: __( 'Number Input' ), icon: 'edit-page', description: __( 'A numeric input.' ), attributes: { type: 'number' }, diff --git a/packages/block-library/src/form-submission-notification/block.json b/packages/block-library/src/form-submission-notification/block.json index e8c3059043c59..5315658d2b008 100644 --- a/packages/block-library/src/form-submission-notification/block.json +++ b/packages/block-library/src/form-submission-notification/block.json @@ -5,7 +5,7 @@ "name": "core/form-submission-notification", "title": "Form Submission Notification", "category": "common", - "parent": [ "core/form" ], + "ancestor": [ "core/form" ], "description": "Provide a notification message after the form has been submitted.", "keywords": [ "form", "feedback", "notification", "message" ], "textdomain": "default", diff --git a/packages/block-library/src/form-submission-notification/variations.js b/packages/block-library/src/form-submission-notification/variations.js index b154a26e5e6a4..f01fccbaa93cf 100644 --- a/packages/block-library/src/form-submission-notification/variations.js +++ b/packages/block-library/src/form-submission-notification/variations.js @@ -6,8 +6,8 @@ import { __ } from '@wordpress/i18n'; const variations = [ { name: 'form-submission-success', - title: __( 'Form submission success' ), - description: __( 'Success message for form submissions' ), + title: __( 'Form Submission Success' ), + description: __( 'Success message for form submissions.' ), attributes: { type: 'success', }, @@ -31,8 +31,8 @@ const variations = [ }, { name: 'form-submission-error', - title: __( 'Form submission error' ), - description: __( 'Error/failure message for form submissions' ), + title: __( 'Form Submission Error' ), + description: __( 'Error/failure message for form submissions.' ), attributes: { type: 'error', }, diff --git a/packages/block-library/src/form-submit-button/block.json b/packages/block-library/src/form-submit-button/block.json index 165742e0e0d44..b3fbd0ccee69c 100644 --- a/packages/block-library/src/form-submit-button/block.json +++ b/packages/block-library/src/form-submit-button/block.json @@ -3,10 +3,10 @@ "apiVersion": 3, "__experimental": true, "name": "core/form-submit-button", - "title": "Form submit button", + "title": "Form Submit Button", "category": "common", "icon": "button", - "parent": [ "core/form" ], + "ancestor": [ "core/form" ], "description": "A submission button for forms.", "keywords": [ "submit", "button", "form" ], "textdomain": "default", diff --git a/packages/block-library/src/form/edit.js b/packages/block-library/src/form/edit.js index 4110cce53f45c..7fded64837de9 100644 --- a/packages/block-library/src/form/edit.js +++ b/packages/block-library/src/form/edit.js @@ -26,6 +26,8 @@ const ALLOWED_BLOCKS = [ 'core/form-input', 'core/form-submit-button', 'core/form-submission-notification', + 'core/group', + 'core/columns', ]; const TEMPLATE = [ diff --git a/packages/block-library/src/form/index.js b/packages/block-library/src/form/index.js index b700e0ade6ca7..a67fc67ca06f0 100644 --- a/packages/block-library/src/form/index.js +++ b/packages/block-library/src/form/index.js @@ -7,6 +7,11 @@ import metadata from './block.json'; import save from './save'; import variations from './variations'; +/** + * WordPress dependencies + */ +import { addFilter } from '@wordpress/hooks'; + const { name } = metadata; export { metadata, name }; @@ -17,4 +22,36 @@ export const settings = { variations, }; -export const init = () => initBlock( { name, metadata, settings } ); +export const init = () => { + // Prevent adding forms inside forms. + const DISALLOWED_PARENTS = [ 'core/form' ]; + addFilter( + 'blockEditor.__unstableCanInsertBlockType', + 'removeTemplatePartsFromPostTemplates', + ( + canInsert, + blockType, + rootClientId, + { getBlock, getBlockParentsByBlockName } + ) => { + if ( blockType.name !== 'core/form' ) { + return canInsert; + } + + for ( const disallowedParentType of DISALLOWED_PARENTS ) { + const hasDisallowedParent = + getBlock( rootClientId )?.name === disallowedParentType || + getBlockParentsByBlockName( + rootClientId, + disallowedParentType + ).length; + if ( hasDisallowedParent ) { + return false; + } + } + return true; + } + ); + + return initBlock( { name, metadata, settings } ); +}; diff --git a/packages/block-library/src/image/image.js b/packages/block-library/src/image/image.js index d42c7c0435cc8..ae5f749fff3b5 100644 --- a/packages/block-library/src/image/image.js +++ b/packages/block-library/src/image/image.js @@ -548,14 +548,14 @@ export default function Image( { { showLightboxToggle && ( !! lightbox } - label={ __( 'Expand on Click' ) } + label={ __( 'Expand on click' ) } onDeselect={ () => { setAttributes( { lightbox: undefined } ); } } isShownByDefault={ true } > { setAttributes( { diff --git a/packages/block-library/src/post-terms/edit.js b/packages/block-library/src/post-terms/edit.js index 49e3a4ce801f4..188a8d22f2f8e 100644 --- a/packages/block-library/src/post-terms/edit.js +++ b/packages/block-library/src/post-terms/edit.js @@ -93,7 +93,7 @@ export default function PostTermsEdit( {
{ isLoading && hasPost && } - { ! isLoading && hasPostTerms && ( isSelected || prefix ) && ( + { ! isLoading && ( isSelected || prefix ) && ( next_tag( array( diff --git a/packages/block-library/src/query-pagination-previous/index.php b/packages/block-library/src/query-pagination-previous/index.php index dc61f5d38b282..fc1fee08e8214 100644 --- a/packages/block-library/src/query-pagination-previous/index.php +++ b/packages/block-library/src/query-pagination-previous/index.php @@ -51,7 +51,7 @@ function render_block_core_query_pagination_previous( $attributes, $content, $bl ); } - if ( $enhanced_pagination ) { + if ( $enhanced_pagination && isset( $content ) ) { $p = new WP_HTML_Tag_Processor( $content ); if ( $p->next_tag( array( diff --git a/packages/block-library/src/query/edit/enhanced-pagination-modal.js b/packages/block-library/src/query/edit/enhanced-pagination-modal.js index 844a86447806c..6009881c7bd86 100644 --- a/packages/block-library/src/query/edit/enhanced-pagination-modal.js +++ b/packages/block-library/src/query/edit/enhanced-pagination-modal.js @@ -12,11 +12,7 @@ import { useState, useEffect } from '@wordpress/element'; /** * Internal dependencies */ -import { useUnsupportedBlockList } from '../utils'; - -const disableEnhancedPaginationDescription = __( - 'You have added unsupported blocks. For the enhanced pagination to work, remove them, then re-enable "Enhanced pagination" in the Query Block settings.' -); +import { useUnsupportedBlocks } from '../utils'; const modalDescriptionId = 'wp-block-query-enhanced-pagination-modal__description'; @@ -27,17 +23,43 @@ export default function EnhancedPaginationModal( { setAttributes, } ) { const [ isOpen, setOpen ] = useState( false ); - - const unsupported = useUnsupportedBlockList( clientId ); + const { hasBlocksFromPlugins, hasPostContentBlock, hasUnsupportedBlocks } = + useUnsupportedBlocks( clientId ); useEffect( () => { - setOpen( !! unsupported.length && enhancedPagination ); - }, [ unsupported.length, enhancedPagination, setOpen ] ); + if ( enhancedPagination && hasUnsupportedBlocks ) { + setAttributes( { enhancedPagination: false } ); + setOpen( true ); + } + }, [ enhancedPagination, hasUnsupportedBlocks, setAttributes ] ); + + const closeModal = () => { + setOpen( false ); + }; + + let notice = __( + 'If you still want to prevent full page reloads, remove that block, then disable "Force page reload" again in the Query Block settings.' + ); + if ( hasBlocksFromPlugins ) { + notice = + __( + 'Currently, avoiding full page reloads is not possible when blocks from plugins are present inside the Query block.' + ) + + ' ' + + notice; + } else if ( hasPostContentBlock ) { + notice = + __( + 'Currently, avoiding full page reloads is not possible when a Content block is present inside the Query block.' + ) + + ' ' + + notice; + } return ( isOpen && ( - - { disableEnhancedPaginationDescription } - - diff --git a/packages/block-library/src/query/edit/inspector-controls/enhanced-pagination-control.js b/packages/block-library/src/query/edit/inspector-controls/enhanced-pagination-control.js index 64b4f5d96eb38..de889c0715c07 100644 --- a/packages/block-library/src/query/edit/inspector-controls/enhanced-pagination-control.js +++ b/packages/block-library/src/query/edit/inspector-controls/enhanced-pagination-control.js @@ -1,58 +1,45 @@ /** * WordPress dependencies */ -import { ToggleControl, Notice } from '@wordpress/components'; +import { ToggleControl } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; -import { BlockTitle } from '@wordpress/block-editor'; /** * Internal dependencies */ -import { useUnsupportedBlockList } from '../../utils'; +import { useUnsupportedBlocks } from '../../utils'; export default function EnhancedPaginationControl( { enhancedPagination, setAttributes, clientId, } ) { - const unsupported = useUnsupportedBlockList( clientId ); + const { hasUnsupportedBlocks } = useUnsupportedBlocks( clientId ); + + let help = __( 'Browsing between pages requires a full page reload.' ); + if ( enhancedPagination ) { + help = __( + "Browsing between pages won't require a full page reload, unless non-compatible blocks are detected." + ); + } else if ( hasUnsupportedBlocks ) { + help = __( + "Force page reload can't be disabled because there are non-compatible blocks inside the Query block." + ); + } return ( <> { setAttributes( { - enhancedPagination: !! value, + enhancedPagination: ! value, } ); } } /> - { !! unsupported.length && ( - - { __( - "Enhanced pagination doesn't support the following blocks:" - ) } -
    - { unsupported.map( ( id ) => ( -
  • - -
  • - ) ) } -
- { __( - 'If you want to enable it, you have to remove all unsupported blocks first.' - ) } -
- ) } ); } diff --git a/packages/block-library/src/query/index.php b/packages/block-library/src/query/index.php index 5e89f21fc3028..201ceed737a12 100644 --- a/packages/block-library/src/query/index.php +++ b/packages/block-library/src/query/index.php @@ -10,14 +10,14 @@ * * @since 6.4.0 * - * @param array $attributes Block attributes. - * @param string $content Block default content. - * @param string $block Block instance. + * @param array $attributes Block attributes. + * @param string $content Block default content. + * @param WP_Block $block The block instance. * * @return string Returns the modified output of the query block. */ function render_block_core_query( $attributes, $content, $block ) { - if ( $attributes['enhancedPagination'] ) { + if ( $attributes['enhancedPagination'] && isset( $attributes['queryId'] ) ) { $p = new WP_HTML_Tag_Processor( $content ); if ( $p->next_tag() ) { // Add the necessary directives. @@ -67,11 +67,14 @@ class="wp-block-query__enhanced-pagination-animation" if ( ! wp_script_is( $view_asset ) ) { $script_handles = $block->block_type->view_script_handles; // If the script is not needed, and it is still in the `view_script_handles`, remove it. - if ( ! $attributes['enhancedPagination'] && in_array( $view_asset, $script_handles, true ) ) { + if ( + ( ! $attributes['enhancedPagination'] || ! isset( $attributes['queryId'] ) ) + && in_array( $view_asset, $script_handles, true ) + ) { $block->block_type->view_script_handles = array_diff( $script_handles, array( $view_asset ) ); } // If the script is needed, but it was previously removed, add it again. - if ( $attributes['enhancedPagination'] && ! in_array( $view_asset, $script_handles, true ) ) { + if ( $attributes['enhancedPagination'] && isset( $attributes['queryId'] ) && ! in_array( $view_asset, $script_handles, true ) ) { $block->block_type->view_script_handles = array_merge( $script_handles, array( $view_asset ) ); } } @@ -80,11 +83,14 @@ class="wp-block-query__enhanced-pagination-animation" if ( ! wp_style_is( $style_asset ) ) { $style_handles = $block->block_type->style_handles; // If the styles are not needed, and they are still in the `style_handles`, remove them. - if ( ! $attributes['enhancedPagination'] && in_array( $style_asset, $style_handles, true ) ) { + if ( + ( ! $attributes['enhancedPagination'] || ! isset( $attributes['queryId'] ) ) + && in_array( $style_asset, $style_handles, true ) + ) { $block->block_type->style_handles = array_diff( $style_handles, array( $style_asset ) ); } // If the styles are needed, but they were previously removed, add them again. - if ( $attributes['enhancedPagination'] && ! in_array( $style_asset, $style_handles, true ) ) { + if ( $attributes['enhancedPagination'] && isset( $attributes['queryId'] ) && ! in_array( $style_asset, $style_handles, true ) ) { $block->block_type->style_handles = array_merge( $style_handles, array( $style_asset ) ); } } @@ -123,3 +129,86 @@ function register_block_core_query() { ); } add_action( 'init', 'register_block_core_query' ); + +/** + * Traverse the tree of blocks looking for any plugin block (i.e., a block from + * an installed plugin) inside a Query block with the enhanced pagination + * enabled. If at least one is found, the enhanced pagination is effectively + * disabled to prevent any potential incompatibilities. + * + * @since 6.4.0 + * + * @param array $parsed_block The block being rendered. + * @return string Returns the parsed block, unmodified. + */ +function block_core_query_disable_enhanced_pagination( $parsed_block ) { + static $enhanced_query_stack = array(); + static $dirty_enhanced_queries = array(); + static $render_query_callback = null; + + $block_name = $parsed_block['blockName']; + + if ( + 'core/query' === $block_name && + isset( $parsed_block['attrs']['enhancedPagination'] ) && + true === $parsed_block['attrs']['enhancedPagination'] && + isset( $parsed_block['attrs']['queryId'] ) + ) { + $enhanced_query_stack[] = $parsed_block['attrs']['queryId']; + + if ( ! isset( $render_query_callback ) ) { + /** + * Filter that disables the enhanced pagination feature during block + * rendering when a plugin block has been found inside. It does so + * by adding an attribute called `data-wp-navigation-disabled` which + * is later handled by the front-end logic. + * + * @param string $content The block content. + * @param array $block The full block, including name and attributes. + * @return string Returns the modified output of the query block. + */ + $render_query_callback = static function ( $content, $block ) use ( &$enhanced_query_stack, &$dirty_enhanced_queries, &$render_query_callback ) { + $has_enhanced_pagination = + isset( $block['attrs']['enhancedPagination'] ) && + true === $block['attrs']['enhancedPagination'] && + isset( $block['attrs']['queryId'] ); + + if ( ! $has_enhanced_pagination ) { + return $content; + } + + if ( isset( $dirty_enhanced_queries[ $block['attrs']['queryId'] ] ) ) { + $p = new WP_HTML_Tag_Processor( $content ); + if ( $p->next_tag() ) { + $p->set_attribute( 'data-wp-navigation-disabled', 'true' ); + } + $content = $p->get_updated_html(); + $dirty_enhanced_queries[ $block['attrs']['queryId'] ] = null; + } + + array_pop( $enhanced_query_stack ); + + if ( empty( $enhanced_query_stack ) ) { + remove_filter( 'render_block_core/query', $render_query_callback ); + $render_query_callback = null; + } + + return $content; + }; + + add_filter( 'render_block_core/query', $render_query_callback, 10, 2 ); + } + } elseif ( + ! empty( $enhanced_query_stack ) && + isset( $block_name ) && + ( ! str_starts_with( $block_name, 'core/' ) || 'core/post-content' === $block_name ) + ) { + foreach ( $enhanced_query_stack as $query_id ) { + $dirty_enhanced_queries[ $query_id ] = true; + } + } + + return $parsed_block; +} + +add_filter( 'render_block_data', 'block_core_query_disable_enhanced_pagination', 10, 1 ); diff --git a/packages/block-library/src/query/utils.js b/packages/block-library/src/query/utils.js index 322c8f4c1453f..2ce4acd4a782c 100644 --- a/packages/block-library/src/query/utils.js +++ b/packages/block-library/src/query/utils.js @@ -346,29 +346,43 @@ export const usePatterns = ( clientId, name ) => { }; /** - * Hook that returns a list of unsupported blocks inside the Query Loop with the - * given `clientId`. + * The object returned by useUnsupportedBlocks with info about the type of + * unsupported blocks present inside the Query block. + * + * @typedef {Object} UnsupportedBlocksInfo + * @property {boolean} hasBlocksFromPlugins True if blocks from plugins are present. + * @property {boolean} hasPostContentBlock True if a 'core/post-content' block is present. + * @property {boolean} hasUnsupportedBlocks True if there are any unsupported blocks. + */ + +/** + * Hook that returns an object with information about the unsupported blocks + * present inside a Query Loop with the given `clientId`. The returned object + * contains props that are true when a certain type of unsupported block is + * present. * * @param {string} clientId The block's client ID. - * @return {string[]} List of block titles. + * @return {UnsupportedBlocksInfo} The object containing the information. */ -export const useUnsupportedBlockList = ( clientId ) => { +export const useUnsupportedBlocks = ( clientId ) => { return useSelect( ( select ) => { const { getClientIdsOfDescendants, getBlockName } = select( blockEditorStore ); - - return getClientIdsOfDescendants( clientId ).filter( + const blocks = {}; + getClientIdsOfDescendants( clientId ).forEach( ( descendantClientId ) => { const blockName = getBlockName( descendantClientId ); - return ( - ! blockName.startsWith( 'core/' ) || - blockName === 'core/post-content' || - blockName === 'core/template-part' || - blockName === 'core/block' - ); + if ( ! blockName.startsWith( 'core/' ) ) { + blocks.hasBlocksFromPlugins = true; + } else if ( blockName === 'core/post-content' ) { + blocks.hasPostContentBlock = true; + } } ); + blocks.hasUnsupportedBlocks = + blocks.hasBlocksFromPlugins || blocks.hasPostContentBlock; + return blocks; }, [ clientId ] ); diff --git a/packages/block-library/src/query/view.js b/packages/block-library/src/query/view.js index 96be4ee513fce..1dac448952b11 100644 --- a/packages/block-library/src/query/view.js +++ b/packages/block-library/src/query/view.js @@ -33,7 +33,14 @@ store( { core: { query: { navigate: async ( { event, ref, context } ) => { - if ( isValidLink( ref ) && isValidEvent( event ) ) { + const isDisabled = ref.closest( '[data-wp-navigation-id]' ) + ?.dataset.wpNavigationDisabled; + + if ( + isValidLink( ref ) && + isValidEvent( event ) && + ! isDisabled + ) { event.preventDefault(); const id = ref.closest( '[data-wp-navigation-id]' ) @@ -70,7 +77,9 @@ store( { } }, prefetch: async ( { ref } ) => { - if ( isValidLink( ref ) ) { + const isDisabled = ref.closest( '[data-wp-navigation-id]' ) + ?.dataset.wpNavigationDisabled; + if ( isValidLink( ref ) && ! isDisabled ) { await prefetch( ref.href ); } }, diff --git a/packages/block-serialization-default-parser/CHANGELOG.md b/packages/block-serialization-default-parser/CHANGELOG.md index 871ec167efdee..7d8b2b6b582ae 100644 --- a/packages/block-serialization-default-parser/CHANGELOG.md +++ b/packages/block-serialization-default-parser/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 4.45.0 (2023-11-02) + ## 4.44.0 (2023-10-18) ## 4.43.0 (2023-10-05) diff --git a/packages/block-serialization-default-parser/package.json b/packages/block-serialization-default-parser/package.json index 80e1399de3917..3072546e446d1 100644 --- a/packages/block-serialization-default-parser/package.json +++ b/packages/block-serialization-default-parser/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/block-serialization-default-parser", - "version": "4.44.0", + "version": "4.45.0", "description": "Block serialization specification parser for WordPress posts.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/block-serialization-spec-parser/CHANGELOG.md b/packages/block-serialization-spec-parser/CHANGELOG.md index b59c4e01cfb8b..f4a3dea0b491b 100644 --- a/packages/block-serialization-spec-parser/CHANGELOG.md +++ b/packages/block-serialization-spec-parser/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 4.45.0 (2023-11-02) + ## 4.44.0 (2023-10-18) ## 4.43.0 (2023-10-05) diff --git a/packages/block-serialization-spec-parser/package.json b/packages/block-serialization-spec-parser/package.json index fdd80bdabf118..1056d871489c3 100644 --- a/packages/block-serialization-spec-parser/package.json +++ b/packages/block-serialization-spec-parser/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/block-serialization-spec-parser", - "version": "4.44.0", + "version": "4.45.0", "description": "Block serialization specification parser for WordPress posts.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/blocks/CHANGELOG.md b/packages/blocks/CHANGELOG.md index 42eaa3e45c246..a0f9d61e0b4c2 100644 --- a/packages/blocks/CHANGELOG.md +++ b/packages/blocks/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 12.22.0 (2023-11-02) + ## 12.21.0 (2023-10-18) ## 12.20.0 (2023-10-05) diff --git a/packages/blocks/package.json b/packages/blocks/package.json index 6e20a2af9848d..813272be7ef6e 100644 --- a/packages/blocks/package.json +++ b/packages/blocks/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/blocks", - "version": "12.21.0", + "version": "12.22.0", "description": "Block API for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/browserslist-config/CHANGELOG.md b/packages/browserslist-config/CHANGELOG.md index e8920dd3541ec..28f88c40569bf 100644 --- a/packages/browserslist-config/CHANGELOG.md +++ b/packages/browserslist-config/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 5.28.0 (2023-11-02) + ## 5.27.0 (2023-10-18) ## 5.26.0 (2023-10-05) diff --git a/packages/browserslist-config/package.json b/packages/browserslist-config/package.json index 06cf5cd24c89c..c7813290dedf5 100644 --- a/packages/browserslist-config/package.json +++ b/packages/browserslist-config/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/browserslist-config", - "version": "5.27.0", + "version": "5.28.0", "description": "WordPress Browserslist shared configuration.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/commands/CHANGELOG.md b/packages/commands/CHANGELOG.md index 9f6a2e38e0dee..a858b5af26543 100644 --- a/packages/commands/CHANGELOG.md +++ b/packages/commands/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 0.16.0 (2023-11-02) + ## 0.15.0 (2023-10-18) ## 0.14.0 (2023-10-05) diff --git a/packages/commands/package.json b/packages/commands/package.json index 445307575567d..0acd14f3de8ad 100644 --- a/packages/commands/package.json +++ b/packages/commands/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/commands", - "version": "0.15.0", + "version": "0.16.0", "description": "Handles the commands menu.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 59a535447bf0d..b3b67164e984e 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 25.11.0 (2023-11-02) + ### Enhancements - `InputControl`/`SelectControl`: update `height`/`min-height` to `32px` instead of `30px` to align with modern sizing scale ([#55490](https://github.com/WordPress/gutenberg/pull/55490)). diff --git a/packages/components/package.json b/packages/components/package.json index a8346b054a6e0..f616b261c8ec4 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/components", - "version": "25.10.0", + "version": "25.11.0", "description": "UI components for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/components/src/slot-fill/bubbles-virtually/slot-fill-provider.tsx b/packages/components/src/slot-fill/bubbles-virtually/slot-fill-provider.tsx index f98ac36c4a2bd..6d10d08aca239 100644 --- a/packages/components/src/slot-fill/bubbles-virtually/slot-fill-provider.tsx +++ b/packages/components/src/slot-fill/bubbles-virtually/slot-fill-provider.tsx @@ -66,7 +66,7 @@ function createSlotRegistry(): SlotFillBubblesVirtuallyContext { const slotFills = fills.get( name ); if ( slotFills ) { // Force update fills. - slotFills.map( ( fill ) => fill.current.rerender() ); + slotFills.forEach( ( fill ) => fill.current.rerender() ); } }; diff --git a/packages/compose/CHANGELOG.md b/packages/compose/CHANGELOG.md index 73f7aedeeafeb..519a4b7b98d9c 100644 --- a/packages/compose/CHANGELOG.md +++ b/packages/compose/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 6.22.0 (2023-11-02) + ## 6.21.0 (2023-10-18) ## 6.20.0 (2023-10-05) diff --git a/packages/compose/package.json b/packages/compose/package.json index c309c5dd72069..17c5ae01ccfb5 100644 --- a/packages/compose/package.json +++ b/packages/compose/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/compose", - "version": "6.21.0", + "version": "6.22.0", "description": "WordPress higher-order components (HOCs).", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/core-commands/CHANGELOG.md b/packages/core-commands/CHANGELOG.md index 05d313a37d3bb..6da22ab229627 100644 --- a/packages/core-commands/CHANGELOG.md +++ b/packages/core-commands/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 0.14.0 (2023-11-02) + ## 0.13.0 (2023-10-18) ## 0.12.0 (2023-10-05) diff --git a/packages/core-commands/package.json b/packages/core-commands/package.json index aa68a285c973c..1ca3d5d25d4d6 100644 --- a/packages/core-commands/package.json +++ b/packages/core-commands/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/core-commands", - "version": "0.13.0", + "version": "0.14.0", "description": "WordPress core reusable commands.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/core-data/CHANGELOG.md b/packages/core-data/CHANGELOG.md index c7d93255e15a7..d6ee748693433 100644 --- a/packages/core-data/CHANGELOG.md +++ b/packages/core-data/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 6.22.0 (2023-11-02) + ## 6.21.0 (2023-10-18) ## Enhancements diff --git a/packages/core-data/package.json b/packages/core-data/package.json index 0e856f3fcbeda..6ab22b7ca9a69 100644 --- a/packages/core-data/package.json +++ b/packages/core-data/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/core-data", - "version": "6.21.0", + "version": "6.22.0", "description": "Access to and manipulation of core WordPress entities.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/create-block-interactive-template/CHANGELOG.md b/packages/create-block-interactive-template/CHANGELOG.md index aceb15e54791a..9a446ed2cfa7e 100644 --- a/packages/create-block-interactive-template/CHANGELOG.md +++ b/packages/create-block-interactive-template/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 1.8.0 (2023-11-02) + ## 1.7.0 (2023-10-18) ## 1.6.0 (2023-10-05) diff --git a/packages/create-block-interactive-template/package.json b/packages/create-block-interactive-template/package.json index 5651f7cd0d2b0..579ad04f6c251 100644 --- a/packages/create-block-interactive-template/package.json +++ b/packages/create-block-interactive-template/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/create-block-interactive-template", - "version": "1.7.0", + "version": "1.8.0", "description": "Template for @wordpress/create-block to create interactive blocks with the Interactivity API.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/create-block-tutorial-template/CHANGELOG.md b/packages/create-block-tutorial-template/CHANGELOG.md index e797e8d1fe3fd..d395b408d86c5 100644 --- a/packages/create-block-tutorial-template/CHANGELOG.md +++ b/packages/create-block-tutorial-template/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 2.33.0 (2023-11-02) + ## 2.32.0 (2023-10-18) ## 2.31.0 (2023-10-05) diff --git a/packages/create-block-tutorial-template/package.json b/packages/create-block-tutorial-template/package.json index e0ca806a827d0..5fd4d71fd4cd8 100644 --- a/packages/create-block-tutorial-template/package.json +++ b/packages/create-block-tutorial-template/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/create-block-tutorial-template", - "version": "2.32.0", + "version": "2.33.0", "description": "Template for @wordpress/create-block used in the official WordPress tutorial.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/create-block/CHANGELOG.md b/packages/create-block/CHANGELOG.md index b6cbfa4938228..7b48fe15b9f2b 100644 --- a/packages/create-block/CHANGELOG.md +++ b/packages/create-block/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 4.29.0 (2023-11-02) + ## 4.28.0 (2023-10-18) ### New Feature diff --git a/packages/create-block/package.json b/packages/create-block/package.json index 9c23b91852136..aad739e103804 100644 --- a/packages/create-block/package.json +++ b/packages/create-block/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/create-block", - "version": "4.28.0", + "version": "4.29.0", "description": "Generates PHP, JS and CSS code for registering a block for a WordPress plugin.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/customize-widgets/CHANGELOG.md b/packages/customize-widgets/CHANGELOG.md index d995fe6f1bc91..3165744b7a6a1 100644 --- a/packages/customize-widgets/CHANGELOG.md +++ b/packages/customize-widgets/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 4.22.0 (2023-11-02) + ## 4.21.0 (2023-10-18) ## 4.20.0 (2023-10-05) diff --git a/packages/customize-widgets/package.json b/packages/customize-widgets/package.json index 6280483df2e79..5f84578f6c6ca 100644 --- a/packages/customize-widgets/package.json +++ b/packages/customize-widgets/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/customize-widgets", - "version": "4.21.0", + "version": "4.22.0", "description": "Widgets blocks in Customizer Module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/data-controls/CHANGELOG.md b/packages/data-controls/CHANGELOG.md index c38c2640c2b0f..3068808b6e028 100644 --- a/packages/data-controls/CHANGELOG.md +++ b/packages/data-controls/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 3.14.0 (2023-11-02) + ## 3.13.0 (2023-10-18) ## 3.12.0 (2023-10-05) diff --git a/packages/data-controls/package.json b/packages/data-controls/package.json index f65a27e66f69e..58ffc68766414 100644 --- a/packages/data-controls/package.json +++ b/packages/data-controls/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/data-controls", - "version": "3.13.0", + "version": "3.14.0", "description": "A set of common controls for the @wordpress/data api.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/data/CHANGELOG.md b/packages/data/CHANGELOG.md index 3c0e653a96838..630a9b01d0dda 100644 --- a/packages/data/CHANGELOG.md +++ b/packages/data/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 9.15.0 (2023-11-02) + ## 9.14.0 (2023-10-18) ## 9.13.1 (2023-10-12) diff --git a/packages/data/package.json b/packages/data/package.json index ffc77d26a8250..b3f4c6b0dd922 100644 --- a/packages/data/package.json +++ b/packages/data/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/data", - "version": "9.14.0", + "version": "9.15.0", "description": "Data module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/date/CHANGELOG.md b/packages/date/CHANGELOG.md index 5d5a4e04292c5..5d7eca2da82e4 100644 --- a/packages/date/CHANGELOG.md +++ b/packages/date/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 4.45.0 (2023-11-02) + ## 4.44.0 (2023-10-18) ## 4.43.0 (2023-10-05) diff --git a/packages/date/package.json b/packages/date/package.json index 4cafb25826c70..6b2e2dfe4280c 100644 --- a/packages/date/package.json +++ b/packages/date/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/date", - "version": "4.44.0", + "version": "4.45.0", "description": "Date module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/dependency-extraction-webpack-plugin/CHANGELOG.md b/packages/dependency-extraction-webpack-plugin/CHANGELOG.md index e643ec36c4f3a..4bc08d6a386b3 100644 --- a/packages/dependency-extraction-webpack-plugin/CHANGELOG.md +++ b/packages/dependency-extraction-webpack-plugin/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 4.28.0 (2023-11-02) + ## 4.27.0 (2023-10-18) ## 4.26.0 (2023-10-05) diff --git a/packages/dependency-extraction-webpack-plugin/package.json b/packages/dependency-extraction-webpack-plugin/package.json index b540de3211250..36eadbaff6335 100644 --- a/packages/dependency-extraction-webpack-plugin/package.json +++ b/packages/dependency-extraction-webpack-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/dependency-extraction-webpack-plugin", - "version": "4.27.0", + "version": "4.28.0", "description": "Extract WordPress script dependencies from webpack bundles.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/deprecated/CHANGELOG.md b/packages/deprecated/CHANGELOG.md index 75c68d9f82fb8..6bd3185175ee7 100644 --- a/packages/deprecated/CHANGELOG.md +++ b/packages/deprecated/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 3.45.0 (2023-11-02) + ## 3.44.0 (2023-10-18) ## 3.43.0 (2023-10-05) diff --git a/packages/deprecated/package.json b/packages/deprecated/package.json index 5a262e489819b..5f13accb2e27b 100644 --- a/packages/deprecated/package.json +++ b/packages/deprecated/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/deprecated", - "version": "3.44.0", + "version": "3.45.0", "description": "Deprecation utility for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/docgen/CHANGELOG.md b/packages/docgen/CHANGELOG.md index 98c280d7dc7f0..0b45b27df3ccb 100644 --- a/packages/docgen/CHANGELOG.md +++ b/packages/docgen/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 1.54.0 (2023-11-02) + ## 1.53.0 (2023-10-18) ## 1.52.0 (2023-10-05) diff --git a/packages/docgen/package.json b/packages/docgen/package.json index 74ecb8461c47d..c8610b86d2cb8 100644 --- a/packages/docgen/package.json +++ b/packages/docgen/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/docgen", - "version": "1.53.0", + "version": "1.54.0", "description": "Autogenerate public API documentation from exports and JSDoc comments.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/dom-ready/CHANGELOG.md b/packages/dom-ready/CHANGELOG.md index d4a268a7005f4..61b414f50d8c3 100644 --- a/packages/dom-ready/CHANGELOG.md +++ b/packages/dom-ready/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 3.45.0 (2023-11-02) + ## 3.44.0 (2023-10-18) ## 3.43.0 (2023-10-05) diff --git a/packages/dom-ready/package.json b/packages/dom-ready/package.json index 5beb59936ad2d..fc7043e2c950f 100644 --- a/packages/dom-ready/package.json +++ b/packages/dom-ready/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/dom-ready", - "version": "3.44.0", + "version": "3.45.0", "description": "Execute callback after the DOM is loaded.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/dom/CHANGELOG.md b/packages/dom/CHANGELOG.md index fb0ffb91f99bf..5a1567021b1ea 100644 --- a/packages/dom/CHANGELOG.md +++ b/packages/dom/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 3.45.0 (2023-11-02) + ## 3.44.0 (2023-10-18) ## 3.43.0 (2023-10-05) diff --git a/packages/dom/package.json b/packages/dom/package.json index b5c70b293408b..b40a161d683b4 100644 --- a/packages/dom/package.json +++ b/packages/dom/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/dom", - "version": "3.44.0", + "version": "3.45.0", "description": "DOM utilities module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/e2e-test-utils-playwright/CHANGELOG.md b/packages/e2e-test-utils-playwright/CHANGELOG.md index 6385b826d8457..cd7425c68eeff 100644 --- a/packages/e2e-test-utils-playwright/CHANGELOG.md +++ b/packages/e2e-test-utils-playwright/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 0.13.0 (2023-11-02) + ## 0.12.0 (2023-10-18) ## 0.11.0 (2023-10-05) diff --git a/packages/e2e-test-utils-playwright/package.json b/packages/e2e-test-utils-playwright/package.json index df70329607249..775e1da771331 100644 --- a/packages/e2e-test-utils-playwright/package.json +++ b/packages/e2e-test-utils-playwright/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/e2e-test-utils-playwright", - "version": "0.12.0", + "version": "0.13.0", "description": "End-To-End (E2E) test utils for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/e2e-test-utils/CHANGELOG.md b/packages/e2e-test-utils/CHANGELOG.md index 824c1a6b9e9b6..bf5d32bb6cc94 100644 --- a/packages/e2e-test-utils/CHANGELOG.md +++ b/packages/e2e-test-utils/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 10.16.0 (2023-11-02) + ## 10.15.0 (2023-10-18) ## 10.14.0 (2023-10-05) diff --git a/packages/e2e-test-utils/package.json b/packages/e2e-test-utils/package.json index 1349b9fe5cd3b..f4bd86e15f1ee 100644 --- a/packages/e2e-test-utils/package.json +++ b/packages/e2e-test-utils/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/e2e-test-utils", - "version": "10.15.0", + "version": "10.16.0", "description": "End-To-End (E2E) test utils for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/e2e-tests/CHANGELOG.md b/packages/e2e-tests/CHANGELOG.md index 0cb1c6531476e..66ab34e826759 100644 --- a/packages/e2e-tests/CHANGELOG.md +++ b/packages/e2e-tests/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 7.16.0 (2023-11-02) + ## 7.15.0 (2023-10-18) ## 7.14.0 (2023-10-05) diff --git a/packages/e2e-tests/package.json b/packages/e2e-tests/package.json index 14cb8503d6dc6..001f395dec608 100644 --- a/packages/e2e-tests/package.json +++ b/packages/e2e-tests/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/e2e-tests", - "version": "7.15.0", + "version": "7.16.0", "description": "End-To-End (E2E) tests for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/e2e-tests/plugins/block-context.php b/packages/e2e-tests/plugins/block-context.php index f640665bb2f0e..a251e9763e525 100644 --- a/packages/e2e-tests/plugins/block-context.php +++ b/packages/e2e-tests/plugins/block-context.php @@ -54,7 +54,7 @@ function gutenberg_test_register_context_blocks() { $block->context['postType'], ); - return implode( ',', $ordered_context ); + return '

' . implode( ',', $ordered_context ) . '

'; }, 'editor_script_handles' => array( 'gutenberg-test-block-context' ), ) 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 274809df3a9e5..7fa79a5091e90 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/directive-init/view.js +++ b/packages/e2e-tests/plugins/interactive-blocks/directive-init/view.js @@ -40,7 +40,6 @@ initOne: ( { context: { isReady, calls } } ) => { isReady[0] = true; // Subscribe to changes in that prop. - isReady[0] = isReady[0]; calls[0]++; }, initTwo: ( { context: { isReady, calls } } ) => { diff --git a/packages/e2e-tests/specs/editor/blocks/site-title.test.js b/packages/e2e-tests/specs/editor/blocks/site-title.test.js deleted file mode 100644 index d24b79b6bb372..0000000000000 --- a/packages/e2e-tests/specs/editor/blocks/site-title.test.js +++ /dev/null @@ -1,87 +0,0 @@ -/** - * WordPress dependencies - */ -import { - createNewPost, - getOption, - insertBlock, - loginUser, - pressKeyWithModifier, - setOption, - openDocumentSettingsSidebar, - canvas, -} from '@wordpress/e2e-test-utils'; - -const saveEntities = async () => { - const savePostSelector = '.editor-post-publish-button__button'; - const savePanelSelector = '.entities-saved-states__panel'; - const entitiesSaveSelector = '.editor-entities-saved-states__save-button'; - const publishPanelSelector = '.editor-post-publish-panel'; - const closePanelButtonSelector = - '.editor-post-publish-panel__header-cancel-button button'; - - await page.click( savePostSelector ); - await page.waitForSelector( savePanelSelector ); - await page.click( entitiesSaveSelector ); - await page.waitForSelector( publishPanelSelector ); - await page.waitForSelector( - '.editor-post-publish-panel__header-cancel-button button:not([disabled])' - ); - await page.click( closePanelButtonSelector ); -}; - -describe( 'Site Title block', () => { - let originalSiteTitle, password; - const username = 'testuser'; - beforeAll( async () => { - originalSiteTitle = await getOption( 'blogname' ); - } ); - - afterAll( async () => { - await setOption( 'blogname', originalSiteTitle ); - } ); - - it( 'Can edit the site title as admin', async () => { - await createNewPost(); - await insertBlock( 'Site Title' ); - const editableSiteTitleSelector = - '[aria-label="Block: Site Title"] a[contenteditable="true"]'; - await canvas().waitForSelector( editableSiteTitleSelector ); - await canvas().focus( editableSiteTitleSelector ); - await pressKeyWithModifier( 'primary', 'a' ); - - await page.keyboard.type( 'New Site Title' ); - - await saveEntities(); - - const siteTitle = await getOption( 'blogname' ); - expect( siteTitle ).toEqual( 'New Site Title' ); - } ); - - // FIXME: Fix https://github.com/WordPress/gutenberg/issues/33003 and enable this test. - // I tried adding an `expect( console ).toHaveErroredWith()` as a workaround, but - // the error occurs only sporadically (e.g. locally in interactive mode, but not in - // headless mode). - it.skip( 'Cannot edit the site title as editor', async () => { - await loginUser( username, password ); - - await createNewPost(); - await insertBlock( 'Site Title' ); - - await openDocumentSettingsSidebar(); - - const [ disableLink ] = await page.$x( - "//label[contains(text(), 'Make title link to home')]" - ); - await disableLink.click(); - - const siteTitleSelector = '[aria-label="Block: Site Title"] span'; - await page.waitForSelector( siteTitleSelector ); - - const editable = await page.$eval( - siteTitleSelector, - ( element ) => element.contentEditable - ); - expect( editable ).toBe( 'inherit' ); - } ); -} ); diff --git a/packages/e2e-tests/specs/editor/plugins/block-context.test.js b/packages/e2e-tests/specs/editor/plugins/block-context.test.js deleted file mode 100644 index 55b905c190dc9..0000000000000 --- a/packages/e2e-tests/specs/editor/plugins/block-context.test.js +++ /dev/null @@ -1,84 +0,0 @@ -/** - * WordPress dependencies - */ -import { - activatePlugin, - createNewPost, - deactivatePlugin, - insertBlock, - saveDraft, - openPreviewPage, -} from '@wordpress/e2e-test-utils'; - -describe( 'Block context', () => { - beforeAll( async () => { - await activatePlugin( 'gutenberg-test-block-context' ); - } ); - - beforeEach( async () => { - await createNewPost(); - } ); - - afterAll( async () => { - await deactivatePlugin( 'gutenberg-test-block-context' ); - } ); - - test( 'Block context propagates to inner blocks', async () => { - await insertBlock( 'Test Context Provider' ); - - // Inserting the context provider block should select the first inner - // block of the template. Verify the contents of the consumer. - let innerBlockText = await page.evaluate( - () => document.activeElement.textContent - ); - expect( innerBlockText ).toBe( 'The record ID is: 0' ); - - // Change the attribute value associated with the context. - await page.keyboard.press( 'ArrowUp' ); - await page.keyboard.type( '123' ); - - // Verify propagated context changes. - await page.keyboard.press( 'ArrowDown' ); - innerBlockText = await page.evaluate( - () => document.activeElement.textContent - ); - expect( innerBlockText ).toBe( 'The record ID is: 123' ); - } ); - - test( 'Block context is reflected in the preview', async () => { - await insertBlock( 'Test Context Provider' ); - const editorPage = page; - const previewPage = await openPreviewPage( editorPage ); - await previewPage.waitForSelector( '.entry-content' ); - - // Check default context values are populated. - let content = await previewPage.$eval( - '.entry-content', - ( contentWrapper ) => contentWrapper.textContent.trim() - ); - expect( content ).toMatch( /^0,\d+,post$/ ); - - // Return to editor to change context value to non-default. - await editorPage.bringToFront(); - await editorPage.focus( - '[data-type="gutenberg/test-context-provider"] input' - ); - await editorPage.keyboard.press( 'ArrowRight' ); - await editorPage.keyboard.type( '123' ); - await editorPage.waitForSelector( '.editor-post-save-draft' ); // Not entirely clear why it's asynchronous, but likely React scheduling prioritizing keyboard event and deferring the UI update. - await saveDraft(); - - // Check non-default context values are populated. - await previewPage.bringToFront(); - await previewPage.reload(); - content = await previewPage.$eval( - '.entry-content', - ( contentWrapper ) => contentWrapper.textContent.trim() - ); - expect( content ).toMatch( /^123,\d+,post$/ ); - - // Clean up. - await editorPage.bringToFront(); - await previewPage.close(); - } ); -} ); diff --git a/packages/e2e-tests/specs/editor/various/publish-button.test.js b/packages/e2e-tests/specs/editor/various/publish-button.test.js deleted file mode 100644 index 90ef0950e535b..0000000000000 --- a/packages/e2e-tests/specs/editor/various/publish-button.test.js +++ /dev/null @@ -1,46 +0,0 @@ -/** - * WordPress dependencies - */ -import { - arePrePublishChecksEnabled, - disablePrePublishChecks, - enablePrePublishChecks, - createNewPost, - canvas, -} from '@wordpress/e2e-test-utils'; - -describe( 'PostPublishButton', () => { - let werePrePublishChecksEnabled; - beforeEach( async () => { - await createNewPost(); - werePrePublishChecksEnabled = await arePrePublishChecksEnabled(); - if ( werePrePublishChecksEnabled ) { - await disablePrePublishChecks(); - } - } ); - - afterEach( async () => { - if ( werePrePublishChecksEnabled ) { - await enablePrePublishChecks(); - } - } ); - - it( 'should be disabled when post is not saveable', async () => { - const publishButton = await page.$( - '.editor-post-publish-button[aria-disabled="true"]' - ); - expect( publishButton ).not.toBeNull(); - } ); - - it( 'should be disabled when post is being saved', async () => { - await canvas().type( '.editor-post-title__input', 'E2E Test Post' ); // Make it saveable. - expect( - await page.$( '.editor-post-publish-button[aria-disabled="true"]' ) - ).toBeNull(); - - await page.click( '.editor-post-save-draft' ); - expect( - await page.$( '.editor-post-publish-button[aria-disabled="true"]' ) - ).not.toBeNull(); - } ); -} ); diff --git a/packages/edit-post/CHANGELOG.md b/packages/edit-post/CHANGELOG.md index 2de5dfae49eb0..6417fcc4454c6 100644 --- a/packages/edit-post/CHANGELOG.md +++ b/packages/edit-post/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 7.22.0 (2023-11-02) + ## 7.21.0 (2023-10-18) ## 7.20.0 (2023-10-05) diff --git a/packages/edit-post/package.json b/packages/edit-post/package.json index ef6dbd0ff833b..ed9b4541103c9 100644 --- a/packages/edit-post/package.json +++ b/packages/edit-post/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/edit-post", - "version": "7.21.0", + "version": "7.22.0", "description": "Edit Post module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/edit-site/CHANGELOG.md b/packages/edit-site/CHANGELOG.md index 73fd0d149a11d..ad970d703cb43 100644 --- a/packages/edit-site/CHANGELOG.md +++ b/packages/edit-site/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 5.22.0 (2023-11-02) + ## 5.21.0 (2023-10-18) ## 5.20.0 (2023-10-05) diff --git a/packages/edit-site/package.json b/packages/edit-site/package.json index 6bb2259330d2e..03d1ea851ff9a 100644 --- a/packages/edit-site/package.json +++ b/packages/edit-site/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/edit-site", - "version": "5.21.0", + "version": "5.22.0", "description": "Edit Site Page module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/edit-site/src/components/actions/index.js b/packages/edit-site/src/components/actions/index.js index 00712567a24dc..6cbe94fc6cd65 100644 --- a/packages/edit-site/src/components/actions/index.js +++ b/packages/edit-site/src/components/actions/index.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { external, trash } from '@wordpress/icons'; +import { external, trash, backup } from '@wordpress/icons'; import { addQueryArgs } from '@wordpress/url'; import { useDispatch } from '@wordpress/data'; import { decodeEntities } from '@wordpress/html-entities'; @@ -67,6 +67,114 @@ export function useTrashPostAction() { ); } +export function usePermanentlyDeletePostAction() { + const { createSuccessNotice, createErrorNotice } = + useDispatch( noticesStore ); + const { deleteEntityRecord } = useDispatch( coreStore ); + + return useMemo( + () => ( { + id: 'permanently-delete', + label: __( 'Permanently delete' ), + isPrimary: true, + icon: trash, + isEligible( { status } ) { + return status === 'trash'; + }, + async perform( post ) { + try { + await deleteEntityRecord( + 'postType', + post.type, + post.id, + { force: true }, + { throwOnError: true } + ); + createSuccessNotice( + sprintf( + /* translators: The posts's title. */ + __( '"%s" permanently deleted.' ), + decodeEntities( post.title.rendered ) + ), + { + type: 'snackbar', + id: 'edit-site-post-permanently-deleted', + } + ); + } catch ( error ) { + const errorMessage = + error.message && error.code !== 'unknown_error' + ? error.message + : __( + 'An error occurred while permanently deleting the post.' + ); + + createErrorNotice( errorMessage, { type: 'snackbar' } ); + } + }, + } ), + [ createSuccessNotice, createErrorNotice, deleteEntityRecord ] + ); +} + +export function useRestorePostAction() { + const { createSuccessNotice, createErrorNotice } = + useDispatch( noticesStore ); + const { editEntityRecord, saveEditedEntityRecord } = + useDispatch( coreStore ); + + return useMemo( + () => ( { + id: 'restore', + label: __( 'Restore' ), + isPrimary: true, + icon: backup, + isEligible( { status } ) { + return status === 'trash'; + }, + async perform( post ) { + await editEntityRecord( 'postType', post.type, post.id, { + status: 'draft', + } ); + try { + await saveEditedEntityRecord( + 'postType', + post.type, + post.id, + { throwOnError: true } + ); + createSuccessNotice( + sprintf( + /* translators: The posts's title. */ + __( '"%s" has been restored.' ), + decodeEntities( post.title.rendered ) + ), + { + type: 'snackbar', + id: 'edit-site-post-restored', + } + ); + } catch ( error ) { + const errorMessage = + error.message && error.code !== 'unknown_error' + ? error.message + : __( + 'An error occurred while restoring the post.' + ); + + createErrorNotice( errorMessage, { type: 'snackbar' } ); + } + }, + } ), + [ + createSuccessNotice, + createErrorNotice, + editEntityRecord, + saveEditedEntityRecord, + ] + ); +} + export const viewPostAction = { id: 'view-post', label: __( 'View' ), diff --git a/packages/edit-site/src/components/app/index.js b/packages/edit-site/src/components/app/index.js index c190c7ef4445e..cad76b3ea1fb8 100644 --- a/packages/edit-site/src/components/app/index.js +++ b/packages/edit-site/src/components/app/index.js @@ -14,7 +14,6 @@ import { privateApis as routerPrivateApis } from '@wordpress/router'; */ import Layout from '../layout'; import { GlobalStylesProvider } from '../global-styles/global-styles-provider'; -import DataviewsProvider from '../dataviews/provider'; import { unlock } from '../../lock-unlock'; const { RouterProvider } = unlock( routerPrivateApis ); @@ -39,10 +38,8 @@ export default function App() { - - - - + + diff --git a/packages/edit-site/src/components/dataviews/README.md b/packages/edit-site/src/components/dataviews/README.md index 460235c8df3df..4b978f1ee7883 100644 --- a/packages/edit-site/src/components/dataviews/README.md +++ b/packages/edit-site/src/components/dataviews/README.md @@ -9,7 +9,6 @@ This file documents the DataViews UI component, which provides an API to render view={ view } onChangeView={ onChangeView } fields={ fields } - filters={ filters } actions={ [ trashPostAction ] } paginationInfo={ { totalItems, totalPages } } /> @@ -43,12 +42,12 @@ Example: field: 'date', direction: 'desc', }, - filters: { - search: '', - author: 2, - status: 'publish, draft' - }, - visibleFilters: [ 'search', 'author', 'status' ], + search: '', + filters: [ + { field: 'author', operator: 'in', value: 2 }, + { field: 'status', operator: 'in', value: 'publish,draft' } + ], + visibleFilters: [ 'author', 'status' ], hiddenFields: [ 'date', 'featured-image' ], layout: {}, } @@ -59,6 +58,7 @@ Example: - `page`: the page that is visible. - `sort.field`: field used for sorting the dataset. - `sort.direction`: the direction to use for sorting, one of `asc` or `desc`. +- `search`: the text search applied to the dataset. - `filters`: the filters applied to the dataset. See filters section. - `visibleFilters`: the `id` of the filters that are visible in the UI. - `hiddenFields`: the `id` of the fields that are hidden in the UI. @@ -76,16 +76,26 @@ function MyCustomPageList() { "...": "..." } ); - const queryArgs = useMemo( - () => ( { + const queryArgs = useMemo( () => { + const filters = {}; + view.filters.forEach( ( filter ) => { + if ( filter.field === 'status' && filter.operator === 'in' ) { + filters.status = filter.value; + } + if ( filter.field === 'author' && filter.operator === 'in' ) { + filters.author = filter.value; + } + } ); + return { per_page: view.perPage, page: view.page, + _embed: 'author', order: view.sort?.direction, - orderby: view.sort?.field - ...view.filters - } ), - [ view ] - ); + orderby: view.sort?.field, + search: view.search, + ...filters, + }; + }, [ view ] ); const { records @@ -133,10 +143,7 @@ Example: { value: 1, label: 'Admin' } { value: 2, label: 'User' } ] - filters: [ - 'enumeration' - { id: 'author_search', type: 'search', name: __( 'Search by author' ) } - ], + filters: [ 'enumeration' ], } ] ``` @@ -150,39 +157,27 @@ Example: ## Filters -Filters describe the conditions a record should match to be listed as part of the dataset. - -Filters can be provided globally, as a property of the `DataViews` component, or per field, should they be considered part of a fields' description. +Filters describe the conditions a record should match to be listed as part of the dataset. Filters are provided per field. ```js const field = [ { id: 'author', - filters: [ - 'enumeration' - { id: 'author_search', type: 'search', name: __( 'Search by author' ) } - ], + filters: [ 'enumeration' ], } ]; ``` A filter is an object that may contain the following properties: -- `id`: unique identifier for the filter. Matches the entity query param. Field filters may omit it, in which case the field's `id` will be used. -- `name`: nice looking name for the filter. Field filters may omit it, in which case the field's `header` will be used. -- `type`: the type of filter. One of `search` or `enumeration`. +- `type`: the type of filter. Only `enumeration` is supported at the moment. - `elements`: for filters of type `enumeration`, the list of options to show. A one-dimensional array of object with value/label keys, as in `[ { value: 1, label: "Value name" } ]`. - `value`: what's serialized into the view's filters. - `label`: nice-looking name for users. -- `resetValue`: for filters of type `enumeration`, this is the value for the reset option. If none is provided, `''` will be used. -- `resetLabel`: for filters of type `enumeration`, this is the label for the reset option. If none is provided, `All` will be used. As a convenience, field's filter can provide abbreviated versions for the filter. All of following examples result in the same filter: @@ -195,9 +190,7 @@ const field = [ filters: [ 'enumeration', { type: 'enumeration' }, - { id: 'author', type: 'enumeration' }, - { id: 'author', type: 'enumeration', name: __( 'Author' ) }, - { id: 'author', type: 'enumeration', name: __( 'Author' ), elements: authors }, + { type: 'enumeration', elements: authors }, ], } ]; diff --git a/packages/edit-site/src/components/dataviews/context.js b/packages/edit-site/src/components/dataviews/context.js deleted file mode 100644 index b7304e3961d93..0000000000000 --- a/packages/edit-site/src/components/dataviews/context.js +++ /dev/null @@ -1,7 +0,0 @@ -/** - * WordPress dependencies - */ -import { createContext } from '@wordpress/element'; - -const DataviewsContext = createContext( {} ); -export default DataviewsContext; diff --git a/packages/edit-site/src/components/dataviews/dataviews.js b/packages/edit-site/src/components/dataviews/dataviews.js index a4e8a5524cceb..81a1224b4c9fe 100644 --- a/packages/edit-site/src/components/dataviews/dataviews.js +++ b/packages/edit-site/src/components/dataviews/dataviews.js @@ -14,13 +14,15 @@ import ViewList from './view-list'; import Pagination from './pagination'; import ViewActions from './view-actions'; import Filters from './filters'; +import Search from './search'; import { ViewGrid } from './view-grid'; export default function DataViews( { view, onChangeView, fields, - filters, + search = true, + searchLabel = undefined, actions, data, isLoading = false, @@ -38,8 +40,14 @@ export default function DataViews( { + { search && ( + + ) } { - if ( 'object' !== typeof filter || ! filter?.id || ! filter?.type ) { - return; - } - - filterIndex[ filter.id ] = filter; - } ); - fields.forEach( ( field ) => { if ( ! field.filters ) { return; } field.filters.forEach( ( filter ) => { - let id = field.id; + const id = field.id; if ( 'string' === typeof filter ) { filterIndex[ id ] = { id, @@ -35,10 +26,9 @@ export default function Filters( { filters, fields, view, onChangeView } ) { } if ( 'object' === typeof filter ) { - id = filter.id || field.id; filterIndex[ id ] = { id, - name: filter.name || field.header, + name: field.header, type: filter.type, }; } @@ -46,8 +36,8 @@ export default function Filters( { filters, fields, view, onChangeView } ) { if ( 'enumeration' === filterIndex[ id ]?.type ) { const elements = [ { - value: filter.resetValue || '', - label: filter.resetLabel || __( 'All' ), + value: '', + label: __( 'All' ), }, ...( field.elements || [] ), ]; @@ -59,36 +49,24 @@ export default function Filters( { filters, fields, view, onChangeView } ) { } ); } ); - return ( - view.visibleFilters?.map( ( filterName ) => { - const filter = filterIndex[ filterName ]; + return view.visibleFilters?.map( ( filterName ) => { + const filter = filterIndex[ filterName ]; - if ( ! filter ) { - return null; - } + if ( ! filter ) { + return null; + } - if ( filter.type === 'search' ) { - return ( - - ); - } - if ( filter.type === 'enumeration' ) { - return ( - - ); - } + if ( filter.type === 'enumeration' ) { + return ( + + ); + } - return null; - } ) || __( 'No filters available' ) - ); + return null; + } ); } diff --git a/packages/edit-site/src/components/dataviews/in-filter.js b/packages/edit-site/src/components/dataviews/in-filter.js index ca9436f5a7ea1..e1f3dcca4c8c9 100644 --- a/packages/edit-site/src/components/dataviews/in-filter.js +++ b/packages/edit-site/src/components/dataviews/in-filter.js @@ -5,19 +5,17 @@ import { __experimentalInputControlPrefixWrapper as InputControlPrefixWrapper, SelectControl, } from '@wordpress/components'; -import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; -/** - * Internal dependencies - */ -import { unlock } from '../../lock-unlock'; - -const { cleanEmptyObject } = unlock( blockEditorPrivateApis ); +const OPERATOR_IN = 'in'; export default ( { filter, view, onChangeView } ) => { + const activeValue = view.filters.find( + ( f ) => f.field === filter.id && f.operator === OPERATOR_IN + )?.value; + return ( { } options={ filter.elements } onChange={ ( value ) => { - if ( value === '' ) { - value = undefined; + const filters = view.filters.filter( + ( f ) => f.field !== filter.id || f.operator !== OPERATOR_IN + ); + if ( value !== '' ) { + filters.push( { + field: filter.id, + operator: OPERATOR_IN, + value, + } ); } onChangeView( ( currentView ) => ( { ...currentView, - filters: cleanEmptyObject( { - ...currentView.filters, - [ filter.id ]: value, - } ), + page: 1, + filters, } ) ); } } /> diff --git a/packages/edit-site/src/components/dataviews/provider.js b/packages/edit-site/src/components/dataviews/provider.js deleted file mode 100644 index f418f8d91834d..0000000000000 --- a/packages/edit-site/src/components/dataviews/provider.js +++ /dev/null @@ -1,270 +0,0 @@ -/** - * WordPress dependencies - */ -import { privateApis as routerPrivateApis } from '@wordpress/router'; -import { - useEffect, - useState, - useRef, - useMemo, - Fragment, -} from '@wordpress/element'; -import { useDispatch, useSelect } from '@wordpress/data'; -import { store as coreStore } from '@wordpress/core-data'; - -/** - * Internal dependencies - */ -import { unlock } from '../../lock-unlock'; -import DataviewsContext from './context'; - -const { useLocation } = unlock( routerPrivateApis ); - -// DEFAULT_STATUSES is intentionally sorted. Items do not have spaces in between them. -// The reason for that is to match the default statuses coming from the endpoint -// (entity request and useEffect to update the view). -export const DEFAULT_STATUSES = 'draft,future,pending,private,publish'; // All statuses but 'trash'. - -const DEFAULT_VIEWS = { - page: { - type: 'list', - filters: { - search: '', - status: DEFAULT_STATUSES, - }, - page: 1, - perPage: 5, - sort: { - field: 'date', - direction: 'desc', - }, - visibleFilters: [ 'search', 'author', 'status' ], - // All fields are visible by default, so it's - // better to keep track of the hidden ones. - hiddenFields: [ 'date', 'featured-image' ], - layout: {}, - }, -}; - -const PATH_TO_DATAVIEW_TYPE = { - '/pages': 'page', -}; - -function useDataviewTypeTaxonomyId( type ) { - const isCreatingATerm = useRef( false ); - const { - dataViewTypeRecords, - dataViewTypeIsResolving, - dataViewTypeIsSaving, - } = useSelect( - ( select ) => { - const { getEntityRecords, isResolving, isSavingEntityRecord } = - select( coreStore ); - const dataViewTypeQuery = { slug: type }; - return { - dataViewTypeRecords: getEntityRecords( - 'taxonomy', - 'wp_dataviews_type', - dataViewTypeQuery - ), - dataViewTypeIsResolving: isResolving( 'getEntityRecords', [ - 'taxonomy', - 'wp_dataviews_type', - dataViewTypeQuery, - ] ), - dataViewTypeIsSaving: isSavingEntityRecord( - 'taxonomy', - 'wp_dataviews_type' - ), - }; - }, - [ type ] - ); - const { saveEntityRecord } = useDispatch( coreStore ); - useEffect( () => { - if ( - dataViewTypeRecords?.length === 0 && - ! dataViewTypeIsResolving && - ! dataViewTypeIsSaving && - ! isCreatingATerm.current - ) { - isCreatingATerm.current = true; - saveEntityRecord( 'taxonomy', 'wp_dataviews_type', { name: type } ); - } - }, [ - type, - dataViewTypeRecords, - dataViewTypeIsResolving, - dataViewTypeIsSaving, - saveEntityRecord, - ] ); - useEffect( () => { - if ( dataViewTypeRecords?.length > 0 ) { - isCreatingATerm.current = false; - } - }, [ dataViewTypeRecords ] ); - useEffect( () => { - isCreatingATerm.current = false; - }, [ type ] ); - if ( dataViewTypeRecords?.length > 0 ) { - return dataViewTypeRecords[ 0 ].id; - } - return null; -} - -function useDataviews( type, typeTaxonomyId ) { - const isCreatingADefaultView = useRef( false ); - const { dataViewRecords, dataViewIsLoading, dataViewIsSaving } = useSelect( - ( select ) => { - const { getEntityRecords, isResolving, isSavingEntityRecord } = - select( coreStore ); - if ( ! typeTaxonomyId ) { - return {}; - } - const dataViewQuery = { - wp_dataviews_type: typeTaxonomyId, - orderby: 'date', - order: 'asc', - }; - return { - dataViewRecords: getEntityRecords( - 'postType', - 'wp_dataviews', - dataViewQuery - ), - dataViewIsLoading: isResolving( 'getEntityRecords', [ - 'postType', - 'wp_dataviews', - dataViewQuery, - ] ), - dataViewIsSaving: isSavingEntityRecord( - 'postType', - 'wp_dataviews' - ), - }; - }, - [ typeTaxonomyId ] - ); - const { saveEntityRecord } = useDispatch( coreStore ); - useEffect( () => { - if ( - dataViewRecords?.length === 0 && - ! dataViewIsLoading && - ! dataViewIsSaving && - ! isCreatingADefaultView.current - ) { - isCreatingADefaultView.current = true; - saveEntityRecord( 'postType', 'wp_dataviews', { - title: 'All', - status: 'publish', - wp_dataviews_type: typeTaxonomyId, - content: JSON.stringify( DEFAULT_VIEWS[ type ] ), - } ); - } - }, [ - type, - dataViewIsLoading, - dataViewRecords, - dataViewIsSaving, - typeTaxonomyId, - saveEntityRecord, - ] ); - useEffect( () => { - if ( dataViewRecords?.length > 0 ) { - isCreatingADefaultView.current = false; - } - }, [ dataViewRecords ] ); - useEffect( () => { - isCreatingADefaultView.current = false; - }, [ typeTaxonomyId ] ); - if ( dataViewRecords?.length > 0 ) { - return dataViewRecords; - } - return null; -} - -function DataviewsProviderInner( { type, children } ) { - const [ currentViewId, setCurrentViewId ] = useState( null ); - const dataviewTypeTaxonomyId = useDataviewTypeTaxonomyId( type ); - const dataviews = useDataviews( type, dataviewTypeTaxonomyId ); - const { editEntityRecord } = useDispatch( coreStore ); - useEffect( () => { - if ( ! currentViewId && dataviews?.length > 0 ) { - setCurrentViewId( dataviews[ 0 ].id ); - } - }, [ currentViewId, dataviews ] ); - const editedViewRecord = useSelect( - ( select ) => { - if ( ! currentViewId ) { - return; - } - const { getEditedEntityRecord } = select( coreStore ); - const dataviewRecord = getEditedEntityRecord( - 'postType', - 'wp_dataviews', - currentViewId - ); - return dataviewRecord; - }, - [ currentViewId ] - ); - - const value = useMemo( () => { - return { - taxonomyId: dataviewTypeTaxonomyId, - dataviews, - currentViewId, - setCurrentViewId, - view: editedViewRecord?.content - ? JSON.parse( editedViewRecord?.content ) - : DEFAULT_VIEWS[ type ], - setView( view ) { - if ( ! currentViewId ) { - return; - } - editEntityRecord( 'postType', 'wp_dataviews', currentViewId, { - content: JSON.stringify( view ), - } ); - }, - }; - }, [ - type, - dataviewTypeTaxonomyId, - dataviews, - currentViewId, - editedViewRecord?.content, - editEntityRecord, - ] ); - - return ( - - { children } - - ); -} -function DataviewsProvider( { children } ) { - const { - params: { path }, - } = useLocation(); - const viewType = PATH_TO_DATAVIEW_TYPE[ path ]; - - if ( viewType ) { - return ( - - { children } - - ); - } - return <>{ children }; -} - -let DataviewsProviderExported = Fragment; - -// This condition must stand on its own for correct tree-shaking -if ( process.env.IS_GUTENBERG_PLUGIN ) { - if ( window?.__experimentalAdminViews ) { - DataviewsProviderExported = DataviewsProvider; - } -} - -export default DataviewsProviderExported; diff --git a/packages/edit-site/src/components/dataviews/text-filter.js b/packages/edit-site/src/components/dataviews/search.js similarity index 74% rename from packages/edit-site/src/components/dataviews/text-filter.js rename to packages/edit-site/src/components/dataviews/search.js index 79ae77c60b71a..3ade147922ac9 100644 --- a/packages/edit-site/src/components/dataviews/text-filter.js +++ b/packages/edit-site/src/components/dataviews/search.js @@ -10,12 +10,12 @@ import { SearchControl } from '@wordpress/components'; */ import useDebouncedInput from '../../utils/use-debounced-input'; -export default function TextFilter( { filter, view, onChangeView } ) { +export default function Search( { label, view, onChangeView } ) { const [ search, setSearch, debouncedSearch ] = useDebouncedInput( - view.filters[ filter.id ] + view.search ); useEffect( () => { - setSearch( view.filters[ filter.id ] ); + setSearch( view.search ); }, [ view ] ); const onChangeViewRef = useRef( onChangeView ); useEffect( () => { @@ -25,13 +25,10 @@ export default function TextFilter( { filter, view, onChangeView } ) { onChangeViewRef.current( ( currentView ) => ( { ...currentView, page: 1, - filters: { - ...currentView.filters, - [ filter.id ]: debouncedSearch, - }, + search: debouncedSearch, } ) ); }, [ debouncedSearch ] ); - const searchLabel = filter?.name || __( 'Filter list' ); + const searchLabel = label || __( 'Filter list' ); return ( { diff --git a/packages/edit-site/src/components/global-styles/ui.js b/packages/edit-site/src/components/global-styles/ui.js index d2e0d2c302706..2e33d4b599b7b 100644 --- a/packages/edit-site/src/components/global-styles/ui.js +++ b/packages/edit-site/src/components/global-styles/ui.js @@ -50,6 +50,7 @@ import { unlock } from '../../lock-unlock'; import { store as editSiteStore } from '../../store'; const SLOT_FILL_NAME = 'GlobalStylesMenu'; +const { useGlobalStylesReset } = unlock( blockEditorPrivateApis ); const { Slot: GlobalStylesMenuSlot, Fill: GlobalStylesMenuFill } = createSlotFill( SLOT_FILL_NAME ); @@ -127,7 +128,6 @@ function GlobalStylesRevisionsMenu() { globalStyles?._links?.[ 'version-history' ]?.[ 0 ]?.count ?? 0, }; }, [] ); - const { useGlobalStylesReset } = unlock( blockEditorPrivateApis ); const [ canReset, onReset ] = useGlobalStylesReset(); const { goTo } = useNavigator(); const { setEditorCanvasContainerView } = unlock( diff --git a/packages/edit-site/src/components/header-edit-mode/document-tools/index.js b/packages/edit-site/src/components/header-edit-mode/document-tools/index.js new file mode 100644 index 0000000000000..397cefdd554e5 --- /dev/null +++ b/packages/edit-site/src/components/header-edit-mode/document-tools/index.js @@ -0,0 +1,219 @@ +/** + * WordPress dependencies + */ +import { useCallback, useRef } from '@wordpress/element'; +import { useViewportMatch } from '@wordpress/compose'; +import { + ToolSelector, + NavigableToolbar, + store as blockEditorStore, + privateApis as blockEditorPrivateApis, +} from '@wordpress/block-editor'; +import { useSelect, useDispatch } from '@wordpress/data'; +import { _x, __ } from '@wordpress/i18n'; +import { listView, plus, chevronUpDown } from '@wordpress/icons'; +import { + __unstableMotion as motion, + Button, + ToolbarItem, +} from '@wordpress/components'; +import { store as keyboardShortcutsStore } from '@wordpress/keyboard-shortcuts'; +import { store as preferencesStore } from '@wordpress/preferences'; + +/** + * Internal dependencies + */ +import UndoButton from '../undo-redo/undo'; +import RedoButton from '../undo-redo/redo'; +import { store as editSiteStore } from '../../../store'; +import { unlock } from '../../../lock-unlock'; + +const { useShouldContextualToolbarShow } = unlock( blockEditorPrivateApis ); + +const preventDefault = ( event ) => { + event.preventDefault(); +}; + +export default function DocumentTools( { + blockEditorMode, + isDistractionFree, + showIconLabels, + setListViewToggleElement, + toolbarTransition, + toolbarVariants, +} ) { + const inserterButton = useRef(); + const { + isInserterOpen, + isListViewOpen, + listViewShortcut, + isVisualMode, + hasFixedToolbar, + } = useSelect( ( select ) => { + const { + __experimentalGetPreviewDeviceType, + isInserterOpened, + isListViewOpened, + getEditorMode, + } = select( editSiteStore ); + const { getShortcutRepresentation } = select( keyboardShortcutsStore ); + + const { get: getPreference } = select( preferencesStore ); + + return { + deviceType: __experimentalGetPreviewDeviceType(), + isInserterOpen: isInserterOpened(), + isListViewOpen: isListViewOpened(), + listViewShortcut: getShortcutRepresentation( + 'core/edit-site/toggle-list-view' + ), + isVisualMode: getEditorMode() === 'visual', + hasFixedToolbar: getPreference( + editSiteStore.name, + 'fixedToolbar' + ), + }; + }, [] ); + + const { + __experimentalSetPreviewDeviceType: setPreviewDeviceType, + setIsInserterOpened, + setIsListViewOpened, + } = useDispatch( editSiteStore ); + const { __unstableSetEditorMode } = useDispatch( blockEditorStore ); + + const isLargeViewport = useViewportMatch( 'medium' ); + + const toggleInserter = useCallback( () => { + if ( isInserterOpen ) { + // Focusing the inserter button should close the inserter popover. + // However, there are some cases it won't close when the focus is lost. + // See https://github.com/WordPress/gutenberg/issues/43090 for more details. + inserterButton.current.focus(); + setIsInserterOpened( false ); + } else { + setIsInserterOpened( true ); + } + }, [ isInserterOpen, setIsInserterOpened ] ); + + const toggleListView = useCallback( + () => setIsListViewOpened( ! isListViewOpen ), + [ setIsListViewOpened, isListViewOpen ] + ); + + const { + shouldShowContextualToolbar, + canFocusHiddenToolbar, + fixedToolbarCanBeFocused, + } = useShouldContextualToolbarShow(); + // If there's a block toolbar to be focused, disable the focus shortcut for the document toolbar. + // There's a fixed block toolbar when the fixed toolbar option is enabled or when the browser width is less than the large viewport. + const blockToolbarCanBeFocused = + shouldShowContextualToolbar || + canFocusHiddenToolbar || + fixedToolbarCanBeFocused; + + /* translators: button label text should, if possible, be under 16 characters. */ + const longLabel = _x( + 'Toggle block inserter', + 'Generic label for block inserter button' + ); + const shortLabel = ! isInserterOpen ? __( 'Add' ) : __( 'Close' ); + + const isZoomedOutViewExperimentEnabled = + window?.__experimentalEnableZoomedOutView && isVisualMode; + const isZoomedOutView = blockEditorMode === 'zoom-out'; + + return ( + +
+ { ! isDistractionFree && ( + + ) } + { isLargeViewport && ( + <> + { ! hasFixedToolbar && ( + + ) } + + + { ! isDistractionFree && ( + + ) } + { isZoomedOutViewExperimentEnabled && + ! isDistractionFree && + ! hasFixedToolbar && ( + { + setPreviewDeviceType( 'Desktop' ); + __unstableSetEditorMode( + isZoomedOutView + ? 'edit' + : 'zoom-out' + ); + } } + /> + ) } + + ) } +
+
+ ); +} diff --git a/packages/edit-site/src/components/header-edit-mode/index.js b/packages/edit-site/src/components/header-edit-mode/index.js index 3528e0623fc7d..81b97f8154c41 100644 --- a/packages/edit-site/src/components/header-edit-mode/index.js +++ b/packages/edit-site/src/components/header-edit-mode/index.js @@ -6,29 +6,22 @@ import classnames from 'classnames'; /** * WordPress dependencies */ -import { useCallback, useRef } from '@wordpress/element'; -import { useViewportMatch, useReducedMotion } from '@wordpress/compose'; +import { useReducedMotion } from '@wordpress/compose'; import { store as coreStore } from '@wordpress/core-data'; import { - ToolSelector, __experimentalPreviewOptions as PreviewOptions, - NavigableToolbar, store as blockEditorStore, - privateApis as blockEditorPrivateApis, } from '@wordpress/block-editor'; import { useSelect, useDispatch } from '@wordpress/data'; import { PinnedItems } from '@wordpress/interface'; -import { _x, __ } from '@wordpress/i18n'; -import { listView, plus, external, chevronUpDown } from '@wordpress/icons'; +import { __ } from '@wordpress/i18n'; +import { external } from '@wordpress/icons'; import { __unstableMotion as motion, - Button, - ToolbarItem, MenuGroup, MenuItem, VisuallyHidden, } from '@wordpress/components'; -import { store as keyboardShortcutsStore } from '@wordpress/keyboard-shortcuts'; import { store as preferencesStore } from '@wordpress/preferences'; /** @@ -36,9 +29,8 @@ import { store as preferencesStore } from '@wordpress/preferences'; */ import MoreMenu from './more-menu'; import SaveButton from '../save-button'; -import UndoButton from './undo-redo/undo'; -import RedoButton from './undo-redo/redo'; import DocumentActions from './document-actions'; +import DocumentTools from './document-tools'; import { store as editSiteStore } from '../../store'; import { getEditorCanvasContainerTitle, @@ -47,36 +39,18 @@ import { import { unlock } from '../../lock-unlock'; import { FOCUSABLE_ENTITIES } from '../../utils/constants'; -const { useShouldContextualToolbarShow } = unlock( blockEditorPrivateApis ); - -const preventDefault = ( event ) => { - event.preventDefault(); -}; - export default function HeaderEditMode( { setListViewToggleElement } ) { - const inserterButton = useRef(); const { deviceType, templateType, - isInserterOpen, - isListViewOpen, - listViewShortcut, - isVisualMode, isDistractionFree, blockEditorMode, homeUrl, showIconLabels, editorCanvasView, - hasFixedToolbar, } = useSelect( ( select ) => { - const { - __experimentalGetPreviewDeviceType, - getEditedPostType, - isInserterOpened, - isListViewOpened, - getEditorMode, - } = select( editSiteStore ); - const { getShortcutRepresentation } = select( keyboardShortcutsStore ); + const { __experimentalGetPreviewDeviceType, getEditedPostType } = + select( editSiteStore ); const { __unstableGetEditorMode } = select( blockEditorStore ); const postType = getEditedPostType(); @@ -90,12 +64,6 @@ export default function HeaderEditMode( { setListViewToggleElement } ) { return { deviceType: __experimentalGetPreviewDeviceType(), templateType: postType, - isInserterOpen: isInserterOpened(), - isListViewOpen: isListViewOpened(), - listViewShortcut: getShortcutRepresentation( - 'core/edit-site/toggle-list-view' - ), - isVisualMode: getEditorMode() === 'visual', blockEditorMode: __unstableGetEditorMode(), homeUrl: getUnstableBase()?.home, showIconLabels: getPreference( @@ -109,65 +77,17 @@ export default function HeaderEditMode( { setListViewToggleElement } ) { editSiteStore.name, 'distractionFree' ), - hasFixedToolbar: getPreference( - editSiteStore.name, - 'fixedToolbar' - ), }; }, [] ); - const { - __experimentalSetPreviewDeviceType: setPreviewDeviceType, - setIsInserterOpened, - setIsListViewOpened, - } = useDispatch( editSiteStore ); - const { __unstableSetEditorMode } = useDispatch( blockEditorStore ); + const { __experimentalSetPreviewDeviceType: setPreviewDeviceType } = + useDispatch( editSiteStore ); const disableMotion = useReducedMotion(); - const isLargeViewport = useViewportMatch( 'medium' ); - - const toggleInserter = useCallback( () => { - if ( isInserterOpen ) { - // Focusing the inserter button should close the inserter popover. - // However, there are some cases it won't close when the focus is lost. - // See https://github.com/WordPress/gutenberg/issues/43090 for more details. - inserterButton.current.focus(); - setIsInserterOpened( false ); - } else { - setIsInserterOpened( true ); - } - }, [ isInserterOpen, setIsInserterOpened ] ); - - const toggleListView = useCallback( - () => setIsListViewOpened( ! isListViewOpen ), - [ setIsListViewOpened, isListViewOpen ] - ); - - const { - shouldShowContextualToolbar, - canFocusHiddenToolbar, - fixedToolbarCanBeFocused, - } = useShouldContextualToolbarShow(); - // If there's a block toolbar to be focused, disable the focus shortcut for the document toolbar. - // There's a fixed block toolbar when the fixed toolbar option is enabled or when the browser width is less than the large viewport. - const blockToolbarCanBeFocused = - shouldShowContextualToolbar || - canFocusHiddenToolbar || - fixedToolbarCanBeFocused; - const hasDefaultEditorCanvasView = ! useHasEditorCanvasContainer(); const isFocusMode = FOCUSABLE_ENTITIES.includes( templateType ); - /* translators: button label text should, if possible, be under 16 characters. */ - const longLabel = _x( - 'Toggle block inserter', - 'Generic label for block inserter button' - ); - const shortLabel = ! isInserterOpen ? __( 'Add' ) : __( 'Close' ); - - const isZoomedOutViewExperimentEnabled = - window?.__experimentalEnableZoomedOutView && isVisualMode; const isZoomedOutView = blockEditorMode === 'zoom-out'; const toolbarVariants = { @@ -190,112 +110,14 @@ export default function HeaderEditMode( { setListViewToggleElement } ) { } ) } > { hasDefaultEditorCanvasView && ( - -
- { ! isDistractionFree && ( - - ) } - { isLargeViewport && ( - <> - { ! hasFixedToolbar && ( - - ) } - - - { ! isDistractionFree && ( - - ) } - { isZoomedOutViewExperimentEnabled && - ! isDistractionFree && - ! hasFixedToolbar && ( - { - setPreviewDeviceType( - 'Desktop' - ); - __unstableSetEditorMode( - isZoomedOutView - ? 'edit' - : 'zoom-out' - ); - } } - /> - ) } - - ) } -
-
+ ) } { ! isDistractionFree && ( diff --git a/packages/edit-site/src/components/layout/index.js b/packages/edit-site/src/components/layout/index.js index 5fb682353da72..7bdb2a31ec4d7 100644 --- a/packages/edit-site/src/components/layout/index.js +++ b/packages/edit-site/src/components/layout/index.js @@ -18,7 +18,7 @@ import { useResizeObserver, } from '@wordpress/compose'; import { __ } from '@wordpress/i18n'; -import { useState, useRef } from '@wordpress/element'; +import { useState } from '@wordpress/element'; import { NavigableRegion } from '@wordpress/interface'; import { store as keyboardShortcutsStore } from '@wordpress/keyboard-shortcuts'; import { @@ -72,7 +72,6 @@ export default function Layout() { useCommonCommands(); useBlockCommands(); - const hubRef = useRef(); const { params } = useLocation(); const isMobileViewport = useViewportMatch( 'medium', '<' ); const isListPage = getIsListPage( params, isMobileViewport ); @@ -226,13 +225,6 @@ export default function Layout() { animate={ headerAnimationState } > diff --git a/packages/edit-site/src/components/page-pages/default-views.js b/packages/edit-site/src/components/page-pages/default-views.js new file mode 100644 index 0000000000000..fda1ef789e3c8 --- /dev/null +++ b/packages/edit-site/src/components/page-pages/default-views.js @@ -0,0 +1,49 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { trash } from '@wordpress/icons'; + +const DEFAULT_PAGE_BASE = { + type: 'list', + search: '', + filters: [], + page: 1, + perPage: 5, + sort: { + field: 'date', + direction: 'desc', + }, + visibleFilters: [ 'author', 'status' ], + // All fields are visible by default, so it's + // better to keep track of the hidden ones. + hiddenFields: [ 'date', 'featured-image' ], + layout: {}, +}; + +const DEFAULT_VIEWS = [ + { + title: __( 'All' ), + slug: 'all', + view: DEFAULT_PAGE_BASE, + }, + { + title: __( 'Drafts' ), + slug: 'drafts', + view: { + ...DEFAULT_PAGE_BASE, + filters: [ { field: 'status', operator: 'in', value: 'draft' } ], + }, + }, + { + title: __( 'Trash' ), + slug: 'trash', + icon: trash, + view: { + ...DEFAULT_PAGE_BASE, + filters: [ { field: 'status', operator: 'in', value: 'trash' } ], + }, + }, +]; + +export default DEFAULT_VIEWS; diff --git a/packages/edit-site/src/components/page-pages/index.js b/packages/edit-site/src/components/page-pages/index.js index e548ddb07f501..7076794f27a7e 100644 --- a/packages/edit-site/src/components/page-pages/index.js +++ b/packages/edit-site/src/components/page-pages/index.js @@ -8,13 +8,9 @@ import { import { __ } from '@wordpress/i18n'; import { useEntityRecords } from '@wordpress/core-data'; import { decodeEntities } from '@wordpress/html-entities'; -import { - useContext, - useMemo, - useCallback, - useEffect, -} from '@wordpress/element'; +import { useState, useMemo, useCallback, useEffect } from '@wordpress/element'; import { dateI18n, getDate, getSettings } from '@wordpress/date'; +import { privateApis as routerPrivateApis } from '@wordpress/router'; /** * Internal dependencies @@ -22,15 +18,18 @@ import { dateI18n, getDate, getSettings } from '@wordpress/date'; import Page from '../page'; import Link from '../routes/link'; import { DataViews } from '../dataviews'; +import { default as DEFAULT_VIEWS } from './default-views'; import { useTrashPostAction, + usePermanentlyDeletePostAction, + useRestorePostAction, postRevisionsAction, viewPostAction, useEditPostAction, } from '../actions'; import Media from '../media'; -import DataviewsContext from '../dataviews/context'; -import { DEFAULT_STATUSES } from '../dataviews/provider'; +import { unlock } from '../../lock-unlock'; +const { useLocation } = unlock( routerPrivateApis ); const EMPTY_ARRAY = []; const defaultConfigPerViewType = { @@ -40,10 +39,25 @@ const defaultConfigPerViewType = { }, }; +// DEFAULT_STATUSES is intentionally sorted. Items do not have spaces in between them. +// The reason for that is to match the default statuses coming from the endpoint (entity request). +export const DEFAULT_STATUSES = 'draft,future,pending,private,publish'; // All statuses but 'trash'. + export default function PagePages() { - const { view, setView } = useContext( DataviewsContext ); - // Request post statuses to get the proper labels. - const { records: statuses } = useEntityRecords( 'root', 'status' ); + const { + params: { path, activeView = 'all' }, + } = useLocation(); + const initialView = DEFAULT_VIEWS.find( + ( { slug } ) => slug === activeView + ).view; + const [ view, setView ] = useState( initialView ); + useEffect( () => { + setView( + DEFAULT_VIEWS.find( ( { slug } ) => slug === activeView ).view + ); + }, [ path, activeView ] ); + const { records: statuses, isResolving: isLoadingStatus } = + useEntityRecords( 'root', 'status' ); const defaultStatuses = useMemo( () => { return statuses === null ? DEFAULT_STATUSES @@ -54,39 +68,32 @@ export default function PagePages() { .join(); }, [ statuses ] ); - useEffect( () => { - // Only update the view if the statuses received from the endpoint - // are different from the DEFAULT_STATUSES provided initially. - // - // The pages endpoint depends on the status endpoint via the status filter. - // Initially, this code filters the pages request by DEFAULT_STATUTES, - // instead of using the default (publish). - // https://developer.wordpress.org/rest-api/reference/pages/#list-pages - // - // By doing so, it avoids a second request to the pages endpoint - // upon receiving the statuses when they are the same (most common scenario). - if ( DEFAULT_STATUSES !== defaultStatuses ) { - setView( { - ...view, - filters: { - ...view.filters, - status: defaultStatuses, - }, - } ); + const queryArgs = useMemo( () => { + const filters = {}; + view.filters.forEach( ( filter ) => { + if ( filter.field === 'status' && filter.operator === 'in' ) { + filters.status = filter.value; + } + if ( filter.field === 'author' && filter.operator === 'in' ) { + filters.author = filter.value; + } + } ); + // We want to provide a different default item for the status filter + // than the REST API provides. + if ( ! filters.status || filters.status === '' ) { + filters.status = defaultStatuses; } - }, [ defaultStatuses ] ); - const queryArgs = useMemo( - () => ( { + return { per_page: view.perPage, page: view.page, _embed: 'author', order: view.sort?.direction, orderby: view.sort?.field, - ...view.filters, - } ), - [ view ] - ); + search: view.search, + ...filters, + }; + }, [ view, defaultStatuses ] ); const { records: pages, isResolving: isLoadingPages, @@ -94,9 +101,8 @@ export default function PagePages() { totalPages, } = useEntityRecords( 'postType', 'page', queryArgs ); - const { records: authors } = useEntityRecords( 'root', 'user', { - has_published_posts: [ 'page' ], - } ); + const { records: authors, isResolving: isLoadingAuthors } = + useEntityRecords( 'root', 'user' ); const paginationInfo = useMemo( () => ( { @@ -178,13 +184,7 @@ export default function PagePages() { getValue: ( { item } ) => statuses?.find( ( { slug } ) => slug === item.status ) ?.name ?? item.status, - filters: [ - { - type: 'enumeration', - id: 'status', - resetValue: defaultStatuses, - }, - ], + filters: [ 'enumeration' ], elements: statuses?.map( ( { slug, name } ) => ( { value: slug, @@ -208,20 +208,25 @@ export default function PagePages() { [ statuses, authors ] ); - const filters = useMemo( () => [ - { id: 'search', type: 'search', name: __( 'Filter list' ) }, - ] ); - const trashPostAction = useTrashPostAction(); + const permanentlyDeletePostAction = usePermanentlyDeletePostAction(); + const restorePostAction = useRestorePostAction(); const editPostAction = useEditPostAction(); const actions = useMemo( () => [ viewPostAction, trashPostAction, + restorePostAction, + permanentlyDeletePostAction, editPostAction, postRevisionsAction, ], - [ trashPostAction, editPostAction ] + [ + trashPostAction, + permanentlyDeletePostAction, + restorePostAction, + editPostAction, + ] ); const onChangeView = useCallback( ( viewUpdater ) => { @@ -249,10 +254,11 @@ export default function PagePages() { diff --git a/packages/edit-site/src/components/page-patterns/rename-category-menu-item.js b/packages/edit-site/src/components/page-patterns/rename-category-menu-item.js index 97e7de069687a..3dc3b4f7919dc 100644 --- a/packages/edit-site/src/components/page-patterns/rename-category-menu-item.js +++ b/packages/edit-site/src/components/page-patterns/rename-category-menu-item.js @@ -5,6 +5,10 @@ import { MenuItem } from '@wordpress/components'; import { useState } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { privateApis as patternsPrivateApis } from '@wordpress/patterns'; +/** + * Internal dependencies + */ +import usePatternCategories from '../sidebar-navigation-screen-patterns/use-pattern-categories'; /** * Internal dependencies @@ -16,30 +20,43 @@ const { RenamePatternCategoryModal } = unlock( patternsPrivateApis ); export default function RenameCategoryMenuItem( { category, onClose } ) { const [ isModalOpen, setIsModalOpen ] = useState( false ); - // User created pattern categories have their properties updated when - // retrieved via `getUserPatternCategories`. The rename modal expects an - // object that will match the pattern category entity. - const normalizedCategory = { - id: category.id, - slug: category.slug, - name: category.label, - }; - return ( <> setIsModalOpen( true ) }> { __( 'Rename' ) } { isModalOpen && ( - { setIsModalOpen( false ); onClose(); } } - overlayClassName="edit-site-list__rename-modal" /> ) } ); } + +function RenameModal( { category, onClose } ) { + // User created pattern categories have their properties updated when + // retrieved via `getUserPatternCategories`. The rename modal expects an + // object that will match the pattern category entity. + const normalizedCategory = { + id: category.id, + slug: category.slug, + name: category.label, + }; + + // Optimization - only use pattern categories when the modal is open. + const existingCategories = usePatternCategories(); + + return ( + + ); +} diff --git a/packages/edit-site/src/components/save-button/index.js b/packages/edit-site/src/components/save-button/index.js index 37d99a1ae1394..455269f073437 100644 --- a/packages/edit-site/src/components/save-button/index.js +++ b/packages/edit-site/src/components/save-button/index.js @@ -22,15 +22,20 @@ export default function SaveButton( { __next40pxDefaultSize = false, } ) { const { isDirty, isSaving, isSaveViewOpen } = useSelect( ( select ) => { - const { __experimentalGetDirtyEntityRecords, isSavingEntityRecord } = - select( coreStore ); + const { + __experimentalGetDirtyEntityRecords, + isSavingEntityRecord, + isResolving, + } = select( coreStore ); const dirtyEntityRecords = __experimentalGetDirtyEntityRecords(); const { isSaveViewOpened } = select( editSiteStore ); + const isActivatingTheme = isResolving( 'activateTheme' ); return { isDirty: dirtyEntityRecords.length > 0, - isSaving: dirtyEntityRecords.some( ( record ) => - isSavingEntityRecord( record.kind, record.name, record.key ) - ), + isSaving: + dirtyEntityRecords.some( ( record ) => + isSavingEntityRecord( record.kind, record.name, record.key ) + ) || isActivatingTheme, isSaveViewOpen: isSaveViewOpened(), }; }, [] ); diff --git a/packages/edit-site/src/components/sidebar-dataviews/index.js b/packages/edit-site/src/components/sidebar-dataviews/index.js new file mode 100644 index 0000000000000..7704c0637b490 --- /dev/null +++ b/packages/edit-site/src/components/sidebar-dataviews/index.js @@ -0,0 +1,66 @@ +/** + * WordPress dependencies + */ +import { __experimentalItemGroup as ItemGroup } from '@wordpress/components'; +import { page, columns } from '@wordpress/icons'; +import { privateApis as routerPrivateApis } from '@wordpress/router'; + +/** + * Internal dependencies + */ +import { useLink } from '../routes/link'; +import { default as DEFAULT_VIEWS } from '../page-pages/default-views'; +import { unlock } from '../../lock-unlock'; +const { useLocation } = unlock( routerPrivateApis ); +import SidebarNavigationItem from '../sidebar-navigation-item'; + +function getDataViewIcon( dataview ) { + const icons = { list: page, grid: columns }; + return icons[ dataview.view.type ]; +} + +function DataViewItem( { dataview, isActive, icon } ) { + const { + params: { path }, + } = useLocation(); + + const _icon = icon || getDataViewIcon( dataview ); + + const linkInfo = useLink( { + path, + activeView: dataview.slug, + } ); + return ( + + { dataview.title } + + ); +} + +export default function DataViewsSidebarContent() { + const { + params: { path, activeView = 'all' }, + } = useLocation(); + if ( ! path || path !== '/pages' ) { + return null; + } + + return ( + + { DEFAULT_VIEWS.map( ( dataview ) => { + return ( + + ); + } ) } + + ); +} diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-page/page-details.js b/packages/edit-site/src/components/sidebar-navigation-screen-page/page-details.js index 9389842391a77..c7862861e9aaf 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-page/page-details.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-page/page-details.js @@ -46,7 +46,9 @@ function getPageDetails( page ) { label: __( 'Slug' ), value: ( - { safeDecodeURIComponent( page.slug ) } + { safeDecodeURIComponent( + page.slug || page.generated_slug + ) } ), }, diff --git a/packages/edit-site/src/components/sidebar/index.js b/packages/edit-site/src/components/sidebar/index.js index 7745b05dbcb64..cbedbabfc3af8 100644 --- a/packages/edit-site/src/components/sidebar/index.js +++ b/packages/edit-site/src/components/sidebar/index.js @@ -29,7 +29,7 @@ import { unlock } from '../../lock-unlock'; import SidebarNavigationScreenPages from '../sidebar-navigation-screen-pages'; import SidebarNavigationScreenPage from '../sidebar-navigation-screen-page'; import SidebarNavigationScreen from '../sidebar-navigation-screen'; -import DataViewsSidebarContent from '../dataviews/sidebar-content'; +import DataViewsSidebarContent from '../sidebar-dataviews'; const { useLocation } = unlock( routerPrivateApis ); diff --git a/packages/edit-site/src/components/site-hub/index.js b/packages/edit-site/src/components/site-hub/index.js index a8a728cfb37ed..9d63001c185c3 100644 --- a/packages/edit-site/src/components/site-hub/index.js +++ b/packages/edit-site/src/components/site-hub/index.js @@ -18,7 +18,7 @@ import { __ } from '@wordpress/i18n'; import { store as blockEditorStore } from '@wordpress/block-editor'; import { store as coreStore } from '@wordpress/core-data'; import { decodeEntities } from '@wordpress/html-entities'; -import { forwardRef } from '@wordpress/element'; +import { memo } from '@wordpress/element'; import { search, external } from '@wordpress/icons'; import { store as commandsStore } from '@wordpress/commands'; import { displayShortcut } from '@wordpress/keycodes'; @@ -32,7 +32,7 @@ import { unlock } from '../../lock-unlock'; const HUB_ANIMATION_DURATION = 0.3; -const SiteHub = forwardRef( ( { isTransparent, ...restProps }, ref ) => { +const SiteHub = memo( ( { isTransparent, className } ) => { const { canvasMode, dashboardLink, homeUrl, siteTitle } = useSelect( ( select ) => { const { getCanvasMode, getSettings } = unlock( @@ -84,12 +84,13 @@ const SiteHub = forwardRef( ( { isTransparent, ...restProps }, ref ) => { return ( { if ( isPreviewingTheme() ) { @@ -30,7 +33,9 @@ export function useActivateTheme() { currentlyPreviewingTheme() + '&_wpnonce=' + window.WP_BLOCK_THEME_ACTIVATE_NONCE; + startResolution( 'activateTheme' ); await window.fetch( activationURL ); + finishResolution( 'activateTheme' ); const { wp_theme_preview: themePreview, ...params } = location.params; history.replace( params ); diff --git a/packages/edit-widgets/CHANGELOG.md b/packages/edit-widgets/CHANGELOG.md index 42d50af442882..e1d1c915a2ad2 100644 --- a/packages/edit-widgets/CHANGELOG.md +++ b/packages/edit-widgets/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 5.22.0 (2023-11-02) + ## 5.21.0 (2023-10-18) ## 5.20.0 (2023-10-05) diff --git a/packages/edit-widgets/package.json b/packages/edit-widgets/package.json index 39159b7fbea2b..754e83f54a5f2 100644 --- a/packages/edit-widgets/package.json +++ b/packages/edit-widgets/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/edit-widgets", - "version": "5.21.0", + "version": "5.22.0", "description": "Widgets Page module for WordPress..", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/edit-widgets/src/components/header/document-tools/index.js b/packages/edit-widgets/src/components/header/document-tools/index.js new file mode 100644 index 0000000000000..8cac7590be5e8 --- /dev/null +++ b/packages/edit-widgets/src/components/header/document-tools/index.js @@ -0,0 +1,130 @@ +/** + * WordPress dependencies + */ +import { useSelect, useDispatch } from '@wordpress/data'; +import { __, _x } from '@wordpress/i18n'; +import { Button, ToolbarItem } from '@wordpress/components'; +import { + NavigableToolbar, + store as blockEditorStore, + privateApis as blockEditorPrivateApis, +} from '@wordpress/block-editor'; +import { listView, plus } from '@wordpress/icons'; +import { useCallback, useRef } from '@wordpress/element'; +import { useViewportMatch } from '@wordpress/compose'; + +/** + * Internal dependencies + */ +import UndoButton from '../undo-redo/undo'; +import RedoButton from '../undo-redo/redo'; +import useLastSelectedWidgetArea from '../../../hooks/use-last-selected-widget-area'; +import { store as editWidgetsStore } from '../../../store'; +import { unlock } from '../../../lock-unlock'; + +const { useShouldContextualToolbarShow } = unlock( blockEditorPrivateApis ); + +function DocumentTools( { setListViewToggleElement } ) { + const isMediumViewport = useViewportMatch( 'medium' ); + const inserterButton = useRef(); + const widgetAreaClientId = useLastSelectedWidgetArea(); + const isLastSelectedWidgetAreaOpen = useSelect( + ( select ) => + select( editWidgetsStore ).getIsWidgetAreaOpen( + widgetAreaClientId + ), + [ widgetAreaClientId ] + ); + const { isInserterOpen, isListViewOpen } = useSelect( ( select ) => { + const { isInserterOpened, isListViewOpened } = + select( editWidgetsStore ); + return { + isInserterOpen: isInserterOpened(), + isListViewOpen: isListViewOpened(), + }; + }, [] ); + const { setIsWidgetAreaOpen, setIsInserterOpened, setIsListViewOpened } = + useDispatch( editWidgetsStore ); + const { selectBlock } = useDispatch( blockEditorStore ); + const handleClick = () => { + if ( isInserterOpen ) { + // Focusing the inserter button closes the inserter popover. + setIsInserterOpened( false ); + } else { + if ( ! isLastSelectedWidgetAreaOpen ) { + // Select the last selected block if hasn't already. + selectBlock( widgetAreaClientId ); + // Open the last selected widget area when opening the inserter. + setIsWidgetAreaOpen( widgetAreaClientId, true ); + } + // The DOM updates resulting from selectBlock() and setIsInserterOpened() calls are applied the + // same tick and pretty much in a random order. The inserter is closed if any other part of the + // app receives focus. If selectBlock() happens to take effect after setIsInserterOpened() then + // the inserter is visible for a brief moment and then gets auto-closed due to focus moving to + // the selected block. + window.requestAnimationFrame( () => setIsInserterOpened( true ) ); + } + }; + + const toggleListView = useCallback( + () => setIsListViewOpened( ! isListViewOpen ), + [ setIsListViewOpened, isListViewOpen ] + ); + + const { + shouldShowContextualToolbar, + canFocusHiddenToolbar, + fixedToolbarCanBeFocused, + } = useShouldContextualToolbarShow(); + // If there's a block toolbar to be focused, disable the focus shortcut for the document toolbar. + // There's a fixed block toolbar when the fixed toolbar option is enabled or when the browser width is less than the large viewport. + const blockToolbarCanBeFocused = + shouldShowContextualToolbar || + canFocusHiddenToolbar || + fixedToolbarCanBeFocused; + + return ( + + { + event.preventDefault(); + } } + onClick={ handleClick } + icon={ plus } + /* translators: button label text should, if possible, be under 16 + characters. */ + label={ _x( + 'Toggle block inserter', + 'Generic label for block inserter button' + ) } + /> + { isMediumViewport && ( + <> + + + + + ) } + + ); +} + +export default DocumentTools; diff --git a/packages/edit-widgets/src/components/header/index.js b/packages/edit-widgets/src/components/header/index.js index 0308c2c2171e2..29afc1eba59b8 100644 --- a/packages/edit-widgets/src/components/header/index.js +++ b/packages/edit-widgets/src/components/header/index.js @@ -1,90 +1,20 @@ /** * WordPress dependencies */ -import { useSelect, useDispatch } from '@wordpress/data'; -import { __, _x } from '@wordpress/i18n'; -import { Button, ToolbarItem, VisuallyHidden } from '@wordpress/components'; -import { - NavigableToolbar, - store as blockEditorStore, - privateApis as blockEditorPrivateApis, -} from '@wordpress/block-editor'; +import { __ } from '@wordpress/i18n'; +import { VisuallyHidden } from '@wordpress/components'; import { PinnedItems } from '@wordpress/interface'; -import { listView, plus } from '@wordpress/icons'; -import { useCallback, useRef } from '@wordpress/element'; import { useViewportMatch } from '@wordpress/compose'; /** * Internal dependencies */ +import DocumentTools from './document-tools'; import SaveButton from '../save-button'; -import UndoButton from './undo-redo/undo'; -import RedoButton from './undo-redo/redo'; import MoreMenu from '../more-menu'; -import useLastSelectedWidgetArea from '../../hooks/use-last-selected-widget-area'; -import { store as editWidgetsStore } from '../../store'; -import { unlock } from '../../lock-unlock'; - -const { useShouldContextualToolbarShow } = unlock( blockEditorPrivateApis ); function Header( { setListViewToggleElement } ) { const isMediumViewport = useViewportMatch( 'medium' ); - const inserterButton = useRef(); - const widgetAreaClientId = useLastSelectedWidgetArea(); - const isLastSelectedWidgetAreaOpen = useSelect( - ( select ) => - select( editWidgetsStore ).getIsWidgetAreaOpen( - widgetAreaClientId - ), - [ widgetAreaClientId ] - ); - const { isInserterOpen, isListViewOpen } = useSelect( ( select ) => { - const { isInserterOpened, isListViewOpened } = - select( editWidgetsStore ); - return { - isInserterOpen: isInserterOpened(), - isListViewOpen: isListViewOpened(), - }; - }, [] ); - const { setIsWidgetAreaOpen, setIsInserterOpened, setIsListViewOpened } = - useDispatch( editWidgetsStore ); - const { selectBlock } = useDispatch( blockEditorStore ); - const handleClick = () => { - if ( isInserterOpen ) { - // Focusing the inserter button closes the inserter popover. - setIsInserterOpened( false ); - } else { - if ( ! isLastSelectedWidgetAreaOpen ) { - // Select the last selected block if hasn't already. - selectBlock( widgetAreaClientId ); - // Open the last selected widget area when opening the inserter. - setIsWidgetAreaOpen( widgetAreaClientId, true ); - } - // The DOM updates resulting from selectBlock() and setIsInserterOpened() calls are applied the - // same tick and pretty much in a random order. The inserter is closed if any other part of the - // app receives focus. If selectBlock() happens to take effect after setIsInserterOpened() then - // the inserter is visible for a brief moment and then gets auto-closed due to focus moving to - // the selected block. - window.requestAnimationFrame( () => setIsInserterOpened( true ) ); - } - }; - - const toggleListView = useCallback( - () => setIsListViewOpened( ! isListViewOpen ), - [ setIsListViewOpened, isListViewOpen ] - ); - - const { - shouldShowContextualToolbar, - canFocusHiddenToolbar, - fixedToolbarCanBeFocused, - } = useShouldContextualToolbarShow(); - // If there's a block toolbar to be focused, disable the focus shortcut for the document toolbar. - // There's a fixed block toolbar when the fixed toolbar option is enabled or when the browser width is less than the large viewport. - const blockToolbarCanBeFocused = - shouldShowContextualToolbar || - canFocusHiddenToolbar || - fixedToolbarCanBeFocused; return ( <> @@ -103,48 +33,9 @@ function Header( { setListViewToggleElement } ) { { __( 'Widgets' ) } ) } - - { - event.preventDefault(); - } } - onClick={ handleClick } - icon={ plus } - /* translators: button label text should, if possible, be under 16 - characters. */ - label={ _x( - 'Toggle block inserter', - 'Generic label for block inserter button' - ) } - /> - { isMediumViewport && ( - <> - - - - - ) } - +
diff --git a/packages/edit-widgets/src/components/widget-areas-block-editor-provider/index.js b/packages/edit-widgets/src/components/widget-areas-block-editor-provider/index.js index d538e37ad1ec8..1fbf51d05f7b7 100644 --- a/packages/edit-widgets/src/components/widget-areas-block-editor-provider/index.js +++ b/packages/edit-widgets/src/components/widget-areas-block-editor-provider/index.js @@ -32,28 +32,36 @@ export default function WidgetAreasBlockEditorProvider( { ...props } ) { const mediaPermissions = useResourcePermissions( 'media' ); - const { reusableBlocks, isFixedToolbarActive, keepCaretInsideBlock } = - useSelect( - ( select ) => ( { - widgetAreas: select( editWidgetsStore ).getWidgetAreas(), - widgets: select( editWidgetsStore ).getWidgets(), - reusableBlocks: ALLOW_REUSABLE_BLOCKS - ? select( coreStore ).getEntityRecords( - 'postType', - 'wp_block' - ) - : [], - isFixedToolbarActive: !! select( preferencesStore ).get( - 'core/edit-widgets', - 'fixedToolbar' - ), - keepCaretInsideBlock: !! select( preferencesStore ).get( - 'core/edit-widgets', - 'keepCaretInsideBlock' - ), - } ), - [] - ); + const { + reusableBlocks, + isFixedToolbarActive, + keepCaretInsideBlock, + pageOnFront, + pageForPosts, + } = useSelect( ( select ) => { + const { canUser, getEntityRecord, getEntityRecords } = + select( coreStore ); + const siteSettings = canUser( 'read', 'settings' ) + ? getEntityRecord( 'root', 'site' ) + : undefined; + return { + widgetAreas: select( editWidgetsStore ).getWidgetAreas(), + widgets: select( editWidgetsStore ).getWidgets(), + reusableBlocks: ALLOW_REUSABLE_BLOCKS + ? getEntityRecords( 'postType', 'wp_block' ) + : [], + isFixedToolbarActive: !! select( preferencesStore ).get( + 'core/edit-widgets', + 'fixedToolbar' + ), + keepCaretInsideBlock: !! select( preferencesStore ).get( + 'core/edit-widgets', + 'keepCaretInsideBlock' + ), + pageOnFront: siteSettings?.page_on_front, + pageForPosts: siteSettings?.page_for_posts, + }; + }, [] ); const { setIsInserterOpened } = useDispatch( editWidgetsStore ); const settings = useMemo( () => { @@ -75,6 +83,8 @@ export default function WidgetAreasBlockEditorProvider( { mediaUpload: mediaUploadBlockEditor, templateLock: 'all', __experimentalSetIsInserterOpened: setIsInserterOpened, + pageOnFront, + pageForPosts, }; }, [ blockEditorSettings, @@ -83,6 +93,8 @@ export default function WidgetAreasBlockEditorProvider( { mediaPermissions.canCreate, reusableBlocks, setIsInserterOpened, + pageOnFront, + pageForPosts, ] ); const widgetAreaId = useLastSelectedWidgetArea(); diff --git a/packages/editor/CHANGELOG.md b/packages/editor/CHANGELOG.md index 3898a0985f04b..0e99e83f46418 100644 --- a/packages/editor/CHANGELOG.md +++ b/packages/editor/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 13.22.0 (2023-11-02) + ## 13.21.0 (2023-10-18) ## 13.20.0 (2023-10-05) diff --git a/packages/editor/package.json b/packages/editor/package.json index 227bf7fd609c2..b45a28c982ee4 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/editor", - "version": "13.21.0", + "version": "13.22.0", "description": "Enhanced block editor for WordPress posts.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/element/CHANGELOG.md b/packages/element/CHANGELOG.md index bbb78076033ee..abc424112d60c 100644 --- a/packages/element/CHANGELOG.md +++ b/packages/element/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 5.22.0 (2023-11-02) + ## 5.21.0 (2023-10-18) ## 5.20.0 (2023-10-05) diff --git a/packages/element/package.json b/packages/element/package.json index ccf3405781150..f1c60680fd156 100644 --- a/packages/element/package.json +++ b/packages/element/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/element", - "version": "5.21.0", + "version": "5.22.0", "description": "Element React module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/env/CHANGELOG.md b/packages/env/CHANGELOG.md index 9b775dc4f7003..67f674c4b6a57 100644 --- a/packages/env/CHANGELOG.md +++ b/packages/env/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 8.11.0 (2023-11-02) + ### Enhancement - `wp-env` now works offline after the environment has been created. Note that many `wp-env` configuration changes involve internet connectivity and may not work in offline mode. [#53547](https://github.com/WordPress/gutenberg/pull/53547) diff --git a/packages/env/package.json b/packages/env/package.json index 94a7a3271666c..42ebbdc9a821f 100644 --- a/packages/env/package.json +++ b/packages/env/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/env", - "version": "8.10.0", + "version": "8.11.0", "description": "A zero-config, self contained local WordPress environment for development and testing.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/escape-html/CHANGELOG.md b/packages/escape-html/CHANGELOG.md index 5a856c31c8d54..d6fa0a8963e1a 100644 --- a/packages/escape-html/CHANGELOG.md +++ b/packages/escape-html/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 2.45.0 (2023-11-02) + ## 2.44.0 (2023-10-18) ## 2.43.0 (2023-10-05) diff --git a/packages/escape-html/package.json b/packages/escape-html/package.json index f5142c64b60cf..473a3bd5511ca 100644 --- a/packages/escape-html/package.json +++ b/packages/escape-html/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/escape-html", - "version": "2.44.0", + "version": "2.45.0", "description": "Escape HTML utils.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/eslint-plugin/CHANGELOG.md b/packages/eslint-plugin/CHANGELOG.md index 7484a2685d470..9b2b7e2bb7173 100644 --- a/packages/eslint-plugin/CHANGELOG.md +++ b/packages/eslint-plugin/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 17.2.0 (2023-11-02) + ## 17.1.0 (2023-10-18) ## 17.0.0 (2023-10-05) diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json index c90f8890e88e3..b262f8801c422 100644 --- a/packages/eslint-plugin/package.json +++ b/packages/eslint-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/eslint-plugin", - "version": "17.1.0", + "version": "17.2.0", "description": "ESLint plugin for WordPress development.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/format-library/CHANGELOG.md b/packages/format-library/CHANGELOG.md index 29745bb1a7b65..9d74bfe94817e 100644 --- a/packages/format-library/CHANGELOG.md +++ b/packages/format-library/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 4.22.0 (2023-11-02) + ## 4.21.0 (2023-10-18) ## 4.20.0 (2023-10-05) diff --git a/packages/format-library/package.json b/packages/format-library/package.json index 0c2cece9b5461..37672549d0b1d 100644 --- a/packages/format-library/package.json +++ b/packages/format-library/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/format-library", - "version": "4.21.0", + "version": "4.22.0", "description": "Format library for the WordPress editor.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/hooks/CHANGELOG.md b/packages/hooks/CHANGELOG.md index 2f682f638c4e1..6ec9ae13bd9bd 100644 --- a/packages/hooks/CHANGELOG.md +++ b/packages/hooks/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 3.45.0 (2023-11-02) + ## 3.44.0 (2023-10-18) ## 3.43.0 (2023-10-05) diff --git a/packages/hooks/package.json b/packages/hooks/package.json index f9f159078f333..879a513e893a6 100644 --- a/packages/hooks/package.json +++ b/packages/hooks/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/hooks", - "version": "3.44.0", + "version": "3.45.0", "description": "WordPress hooks library.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/html-entities/CHANGELOG.md b/packages/html-entities/CHANGELOG.md index 45c2b61058858..4cb861f9ad09e 100644 --- a/packages/html-entities/CHANGELOG.md +++ b/packages/html-entities/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 3.45.0 (2023-11-02) + ## 3.44.0 (2023-10-18) ## 3.43.0 (2023-10-05) diff --git a/packages/html-entities/package.json b/packages/html-entities/package.json index a9fe129374bc5..5ff780e4b5cb1 100644 --- a/packages/html-entities/package.json +++ b/packages/html-entities/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/html-entities", - "version": "3.44.0", + "version": "3.45.0", "description": "HTML entity utilities for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/i18n/CHANGELOG.md b/packages/i18n/CHANGELOG.md index 05565f79f1afb..e34507334f2aa 100644 --- a/packages/i18n/CHANGELOG.md +++ b/packages/i18n/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 4.45.0 (2023-11-02) + ## 4.44.0 (2023-10-18) ## 4.43.0 (2023-10-05) diff --git a/packages/i18n/package.json b/packages/i18n/package.json index ede1c8e8edd09..2b53debe4f79a 100644 --- a/packages/i18n/package.json +++ b/packages/i18n/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/i18n", - "version": "4.44.0", + "version": "4.45.0", "description": "WordPress internationalization (i18n) library.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/icons/CHANGELOG.md b/packages/icons/CHANGELOG.md index f5c37c58b2e50..0b7ccc517defa 100644 --- a/packages/icons/CHANGELOG.md +++ b/packages/icons/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 9.36.0 (2023-11-02) + ## 9.35.0 (2023-10-18) ## 9.34.0 (2023-10-05) diff --git a/packages/icons/package.json b/packages/icons/package.json index 6f6b05939dac5..b0117b28edca0 100644 --- a/packages/icons/package.json +++ b/packages/icons/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/icons", - "version": "9.35.0", + "version": "9.36.0", "description": "WordPress Icons package, based on dashicon.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/interactivity/CHANGELOG.md b/packages/interactivity/CHANGELOG.md index a7f3180b2172a..7b5faaeefa1b2 100644 --- a/packages/interactivity/CHANGELOG.md +++ b/packages/interactivity/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 2.6.0 (2023-11-02) + ### Bug Fix - Update the title when using enhanced pagination. ([#55446](https://github.com/WordPress/gutenberg/pull/55446)) diff --git a/packages/interactivity/package.json b/packages/interactivity/package.json index 2d7cdbeb8cbf2..0580868b7d714 100644 --- a/packages/interactivity/package.json +++ b/packages/interactivity/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/interactivity", - "version": "2.5.0", + "version": "2.6.0", "description": "Package that provides a standard and simple way to handle the frontend interactivity of Gutenberg blocks.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/interface/CHANGELOG.md b/packages/interface/CHANGELOG.md index 20d7270b6323f..73737f3ed99e9 100644 --- a/packages/interface/CHANGELOG.md +++ b/packages/interface/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 5.22.0 (2023-11-02) + ## 5.21.0 (2023-10-18) ## 5.20.0 (2023-10-05) diff --git a/packages/interface/package.json b/packages/interface/package.json index e365d892a170e..28b11d1cbee5f 100644 --- a/packages/interface/package.json +++ b/packages/interface/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/interface", - "version": "5.21.0", + "version": "5.22.0", "description": "Interface module for WordPress. The package contains shared functionality across the modern JavaScript-based WordPress screens.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/is-shallow-equal/CHANGELOG.md b/packages/is-shallow-equal/CHANGELOG.md index eec33c4ed28b6..9eef05af9f1d6 100644 --- a/packages/is-shallow-equal/CHANGELOG.md +++ b/packages/is-shallow-equal/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 4.45.0 (2023-11-02) + ## 4.44.0 (2023-10-18) ## 4.43.0 (2023-10-05) diff --git a/packages/is-shallow-equal/package.json b/packages/is-shallow-equal/package.json index dc41cbd166887..e6b553baaceb7 100644 --- a/packages/is-shallow-equal/package.json +++ b/packages/is-shallow-equal/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/is-shallow-equal", - "version": "4.44.0", + "version": "4.45.0", "description": "Test for shallow equality between two objects or arrays.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/jest-console/CHANGELOG.md b/packages/jest-console/CHANGELOG.md index e80d21ea86faf..463bfd52db437 100644 --- a/packages/jest-console/CHANGELOG.md +++ b/packages/jest-console/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 7.16.0 (2023-11-02) + ## 7.15.0 (2023-10-18) ## 7.14.0 (2023-10-05) diff --git a/packages/jest-console/package.json b/packages/jest-console/package.json index ff716e24d59f6..40981febe941f 100644 --- a/packages/jest-console/package.json +++ b/packages/jest-console/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/jest-console", - "version": "7.15.0", + "version": "7.16.0", "description": "Custom Jest matchers for the Console object.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/jest-preset-default/CHANGELOG.md b/packages/jest-preset-default/CHANGELOG.md index f80a3d824e44b..a56d34dddad38 100644 --- a/packages/jest-preset-default/CHANGELOG.md +++ b/packages/jest-preset-default/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 11.16.0 (2023-11-02) + ## 11.15.0 (2023-10-18) ## 11.14.0 (2023-10-05) diff --git a/packages/jest-preset-default/package.json b/packages/jest-preset-default/package.json index 547578e3c6a2b..ecc0769060356 100644 --- a/packages/jest-preset-default/package.json +++ b/packages/jest-preset-default/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/jest-preset-default", - "version": "11.15.0", + "version": "11.16.0", "description": "Default Jest preset for WordPress development.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/jest-puppeteer-axe/CHANGELOG.md b/packages/jest-puppeteer-axe/CHANGELOG.md index 56d6d4113c999..acf19522a59bc 100644 --- a/packages/jest-puppeteer-axe/CHANGELOG.md +++ b/packages/jest-puppeteer-axe/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 6.16.0 (2023-11-02) + ## 6.15.0 (2023-10-18) ## 6.14.0 (2023-10-05) diff --git a/packages/jest-puppeteer-axe/package.json b/packages/jest-puppeteer-axe/package.json index 28cad81adfb33..68af3143838f3 100644 --- a/packages/jest-puppeteer-axe/package.json +++ b/packages/jest-puppeteer-axe/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/jest-puppeteer-axe", - "version": "6.15.0", + "version": "6.16.0", "description": "Axe API integration with Jest and Puppeteer.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/keyboard-shortcuts/CHANGELOG.md b/packages/keyboard-shortcuts/CHANGELOG.md index 93542a6b9ec33..b5988c7102d03 100644 --- a/packages/keyboard-shortcuts/CHANGELOG.md +++ b/packages/keyboard-shortcuts/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 4.22.0 (2023-11-02) + ## 4.21.0 (2023-10-18) ## 4.20.0 (2023-10-05) diff --git a/packages/keyboard-shortcuts/package.json b/packages/keyboard-shortcuts/package.json index 33f9d7fe6ba27..03100a616bc4a 100644 --- a/packages/keyboard-shortcuts/package.json +++ b/packages/keyboard-shortcuts/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/keyboard-shortcuts", - "version": "4.21.0", + "version": "4.22.0", "description": "Handling keyboard shortcuts.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/keycodes/CHANGELOG.md b/packages/keycodes/CHANGELOG.md index 7ae7d2b972a49..3ab5dc90aa858 100644 --- a/packages/keycodes/CHANGELOG.md +++ b/packages/keycodes/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 3.45.0 (2023-11-02) + ## 3.44.0 (2023-10-18) ## 3.43.0 (2023-10-05) diff --git a/packages/keycodes/package.json b/packages/keycodes/package.json index 9ca4af5ed7d55..f3705c6e523c1 100644 --- a/packages/keycodes/package.json +++ b/packages/keycodes/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/keycodes", - "version": "3.44.0", + "version": "3.45.0", "description": "Keycodes utilities for WordPress. Used to check for keyboard events across browsers/operating systems.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/lazy-import/CHANGELOG.md b/packages/lazy-import/CHANGELOG.md index e48b809b048c2..e96828769051f 100644 --- a/packages/lazy-import/CHANGELOG.md +++ b/packages/lazy-import/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 1.32.0 (2023-11-02) + ## 1.31.0 (2023-10-18) ## 1.30.0 (2023-10-05) diff --git a/packages/lazy-import/package.json b/packages/lazy-import/package.json index 9aec1f001e8de..f697a2c259598 100644 --- a/packages/lazy-import/package.json +++ b/packages/lazy-import/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/lazy-import", - "version": "1.31.0", + "version": "1.32.0", "description": "Lazily import a module, installing it automatically if missing.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/list-reusable-blocks/CHANGELOG.md b/packages/list-reusable-blocks/CHANGELOG.md index 836d352a85d91..bb86ca45b5965 100644 --- a/packages/list-reusable-blocks/CHANGELOG.md +++ b/packages/list-reusable-blocks/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 4.22.0 (2023-11-02) + ## 4.21.0 (2023-10-18) ## 4.20.0 (2023-10-05) diff --git a/packages/list-reusable-blocks/package.json b/packages/list-reusable-blocks/package.json index 0083572ab4be4..b004d5fe803ee 100644 --- a/packages/list-reusable-blocks/package.json +++ b/packages/list-reusable-blocks/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/list-reusable-blocks", - "version": "4.21.0", + "version": "4.22.0", "description": "Adding Export/Import support to the reusable blocks listing.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/media-utils/CHANGELOG.md b/packages/media-utils/CHANGELOG.md index 3d40b357ca882..bc1ea49c74c86 100644 --- a/packages/media-utils/CHANGELOG.md +++ b/packages/media-utils/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 4.36.0 (2023-11-02) + ## 4.35.0 (2023-10-18) ## 4.34.0 (2023-10-05) diff --git a/packages/media-utils/package.json b/packages/media-utils/package.json index 1bf19da846ae0..16477d6546ad7 100644 --- a/packages/media-utils/package.json +++ b/packages/media-utils/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/media-utils", - "version": "4.35.0", + "version": "4.36.0", "description": "WordPress Media Upload Utils.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/notices/CHANGELOG.md b/packages/notices/CHANGELOG.md index 21aa2efb84576..f22d214e00037 100644 --- a/packages/notices/CHANGELOG.md +++ b/packages/notices/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 4.13.0 (2023-11-02) + ## 4.12.0 (2023-10-18) ## 4.11.0 (2023-10-05) diff --git a/packages/notices/package.json b/packages/notices/package.json index 4f5185a9c5917..798f2120b94df 100644 --- a/packages/notices/package.json +++ b/packages/notices/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/notices", - "version": "4.12.0", + "version": "4.13.0", "description": "State management for notices.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/npm-package-json-lint-config/CHANGELOG.md b/packages/npm-package-json-lint-config/CHANGELOG.md index 869a34d29013f..1242e51b295a6 100644 --- a/packages/npm-package-json-lint-config/CHANGELOG.md +++ b/packages/npm-package-json-lint-config/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 4.30.0 (2023-11-02) + ## 4.29.0 (2023-10-18) ## 4.28.0 (2023-10-05) diff --git a/packages/npm-package-json-lint-config/package.json b/packages/npm-package-json-lint-config/package.json index 06cdcb200888d..a3f4a0dfcc930 100644 --- a/packages/npm-package-json-lint-config/package.json +++ b/packages/npm-package-json-lint-config/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/npm-package-json-lint-config", - "version": "4.29.0", + "version": "4.30.0", "description": "WordPress npm-package-json-lint shareable configuration.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/nux/CHANGELOG.md b/packages/nux/CHANGELOG.md index 9b6ceb7446b9e..fa995b4dbcf54 100644 --- a/packages/nux/CHANGELOG.md +++ b/packages/nux/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 8.7.0 (2023-11-02) + ## 8.6.0 (2023-10-18) ## 8.5.0 (2023-10-05) diff --git a/packages/nux/package.json b/packages/nux/package.json index 64625113cd82d..f03f91dcf4c8b 100644 --- a/packages/nux/package.json +++ b/packages/nux/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/nux", - "version": "8.6.0", + "version": "8.7.0", "description": "NUX (New User eXperience) module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/patterns/CHANGELOG.md b/packages/patterns/CHANGELOG.md index 313aff90b9eb1..3a9c684d01b04 100644 --- a/packages/patterns/CHANGELOG.md +++ b/packages/patterns/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 1.6.0 (2023-11-02) + ## 1.5.0 (2023-10-18) ## 1.4.0 (2023-10-05) diff --git a/packages/patterns/package.json b/packages/patterns/package.json index 7ee08ea007b12..41846c1047d93 100644 --- a/packages/patterns/package.json +++ b/packages/patterns/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/patterns", - "version": "1.5.0", + "version": "1.6.0", "description": "Management of user pattern editing.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", @@ -31,6 +31,7 @@ ], "dependencies": { "@babel/runtime": "^7.16.0", + "@wordpress/a11y": "file:../a11y", "@wordpress/block-editor": "file:../block-editor", "@wordpress/blocks": "file:../blocks", "@wordpress/components": "file:../components", diff --git a/packages/patterns/src/components/rename-pattern-category-modal.js b/packages/patterns/src/components/rename-pattern-category-modal.js index e4015f259246b..3e9e90da2f821 100644 --- a/packages/patterns/src/components/rename-pattern-category-modal.js +++ b/packages/patterns/src/components/rename-pattern-category-modal.js @@ -10,10 +10,11 @@ import { } from '@wordpress/components'; import { store as coreStore } from '@wordpress/core-data'; import { useDispatch } from '@wordpress/data'; -import { useState } from '@wordpress/element'; +import { useId, useRef, useState } from '@wordpress/element'; import { decodeEntities } from '@wordpress/html-entities'; import { __ } from '@wordpress/i18n'; import { store as noticesStore } from '@wordpress/notices'; +import { speak } from '@wordpress/a11y'; /** * Internal dependencies @@ -22,23 +23,64 @@ import { CATEGORY_SLUG } from './category-selector'; export default function RenamePatternCategoryModal( { category, + existingCategories, onClose, onError, onSuccess, ...props } ) { + const id = useId(); + const textControlRef = useRef(); const [ name, setName ] = useState( decodeEntities( category.name ) ); const [ isSaving, setIsSaving ] = useState( false ); + const [ validationMessage, setValidationMessage ] = useState( false ); + const validationMessageId = validationMessage + ? `patterns-rename-pattern-category-modal__validation-message-${ id }` + : undefined; const { saveEntityRecord, invalidateResolution } = useDispatch( coreStore ); - const { createErrorNotice, createSuccessNotice } = useDispatch( noticesStore ); - const onRename = async ( event ) => { + const onChange = ( newName ) => { + if ( validationMessage ) { + setValidationMessage( undefined ); + } + setName( newName ); + }; + + const onSave = async ( event ) => { event.preventDefault(); - if ( ! name || name === category.name || isSaving ) { + if ( isSaving ) { + return; + } + + if ( ! name || name === category.name ) { + const message = __( 'Please enter a new name for this category.' ); + speak( message, 'assertive' ); + setValidationMessage( message ); + textControlRef.current?.focus(); + return; + } + + // Check existing categories to avoid creating duplicates. + if ( + existingCategories.patternCategories.find( ( existingCategory ) => { + // Compare the id so that the we don't disallow the user changing the case of their current category + // (i.e. renaming 'test' to 'Test'). + return ( + existingCategory.id !== category.id && + existingCategory.label.toLowerCase() === name.toLowerCase() + ); + } ) + ) { + const message = __( + 'This category already exists. Please use a different name.' + ); + speak( message, 'assertive' ); + setValidationMessage( message ); + textControlRef.current?.focus(); return; } @@ -90,15 +132,27 @@ export default function RenamePatternCategoryModal( { onRequestClose={ onRequestClose } { ...props } > -
+ - + + + { validationMessage && ( + + { validationMessage } + + ) } +
' ); - $this->assertFalse( $p->has_bookmark( 'my-bookmark' ) ); - } - - /** - * @covers has_bookmark - */ - public function test_has_bookmark_returns_true_if_bookmark_exists() { - $p = new Gutenberg_HTML_Tag_Processor_6_3( '
Test
' ); - $p->next_tag(); - $p->set_bookmark( 'my-bookmark' ); - $this->assertTrue( $p->has_bookmark( 'my-bookmark' ) ); - } - - /** - * @covers has_bookmark - */ - public function test_has_bookmark_returns_false_if_bookmark_has_been_released() { - $p = new Gutenberg_HTML_Tag_Processor_6_3( '
Test
' ); - $p->next_tag(); - $p->set_bookmark( 'my-bookmark' ); - $p->release_bookmark( 'my-bookmark' ); - $this->assertFalse( $p->has_bookmark( 'my-bookmark' ) ); - } -} diff --git a/test/e2e/specs/editor/blocks/site-title.spec.js b/test/e2e/specs/editor/blocks/site-title.spec.js new file mode 100644 index 0000000000000..acc3ada55ce8c --- /dev/null +++ b/test/e2e/specs/editor/blocks/site-title.spec.js @@ -0,0 +1,46 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +test.describe( 'Site Title block', () => { + let originalSiteTitle; + + test.beforeAll( async ( { requestUtils } ) => { + originalSiteTitle = ( await requestUtils.getSiteSettings() ).title; + } ); + + test.afterAll( async ( { requestUtils } ) => { + await requestUtils.updateSiteSettings( { title: originalSiteTitle } ); + } ); + + test( 'Can edit the site title as admin', async ( { + admin, + editor, + page, + } ) => { + await admin.createNewPost(); + await editor.insertBlock( { name: 'core/site-title' } ); + + const siteTitleBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Site Title', + } ); + + // Update the site title + await siteTitleBlock + .getByRole( 'textbox', { + name: 'Site title text', + } ) + .fill( 'New Site Title' ); + + await editor.publishPost(); + await page.reload(); + + await expect( siteTitleBlock ).toBeVisible(); + await expect( siteTitleBlock ).toHaveText( 'New Site Title' ); + } ); + + // Reason: The current e2e test setup doesn't provide an easy way to switch between user roles. + // eslint-disable-next-line playwright/expect-expect + test.fixme( 'Cannot edit the site title as editor', async () => {} ); +} ); diff --git a/test/e2e/specs/editor/plugins/block-context.spec.js b/test/e2e/specs/editor/plugins/block-context.spec.js new file mode 100644 index 0000000000000..1fc91debd1145 --- /dev/null +++ b/test/e2e/specs/editor/plugins/block-context.spec.js @@ -0,0 +1,78 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +test.describe( 'Block context', () => { + test.beforeAll( async ( { requestUtils } ) => { + await requestUtils.activatePlugin( 'gutenberg-test-block-context' ); + } ); + + test.beforeEach( async ( { admin } ) => { + await admin.createNewPost(); + } ); + + test.afterAll( async ( { requestUtils } ) => { + await requestUtils.deactivatePlugin( 'gutenberg-test-block-context' ); + } ); + + test( 'Block context propagates to inner blocks', async ( { + editor, + page, + } ) => { + await editor.insertBlock( { name: 'gutenberg/test-context-provider' } ); + + const providerBlock = page.getByRole( 'document', { + name: 'Block: Test Context Provider', + } ); + const consumerBlock = page.getByRole( 'document', { + name: 'Block: Test Context Consumer', + } ); + + await expect( consumerBlock ).toBeVisible(); + + // Verify initial contents of consumer. + await expect( consumerBlock ).toHaveText( 'The record ID is: 0' ); + + // Change the attribute value associated with the context. + await providerBlock.getByRole( 'textbox' ).fill( '123' ); + + await expect( consumerBlock ).toHaveText( 'The record ID is: 123' ); + } ); + + test( 'Block context is reflected in the preview', async ( { + editor, + page, + } ) => { + const editorPage = page; + + await editor.insertBlock( { name: 'gutenberg/test-context-provider' } ); + + // Open the preview page. + const previewPage = await editor.openPreviewPage(); + const previewContent = previewPage.locator( '.entry-content p' ); + + await expect( previewContent ).toHaveText( /^0,\d+,post$/ ); + + // Return to editor to change context value to non-default. + await editorPage.bringToFront(); + await editorPage + .getByRole( 'document', { + name: 'Block: Test Context Provider', + } ) + .getByRole( 'textbox' ) + .fill( '123' ); + + await editorPage + .getByRole( 'button', { name: 'Preview', expanded: false } ) + .click(); + await editorPage + .getByRole( 'menuitem', { name: 'Preview in new tab' } ) + .click(); + + // Check non-default context values are populated. + await expect( previewContent ).toHaveText( /^123,\d+,post$/ ); + await editorPage.bringToFront(); + await previewPage.close(); + } ); +} ); diff --git a/test/e2e/specs/editor/various/block-switcher-test.spec.js b/test/e2e/specs/editor/various/block-switcher.spec.js similarity index 100% rename from test/e2e/specs/editor/various/block-switcher-test.spec.js rename to test/e2e/specs/editor/various/block-switcher.spec.js diff --git a/test/e2e/specs/editor/various/navigable-toolbar.spec.js b/test/e2e/specs/editor/various/navigable-toolbar.spec.js index abdb1800d150a..0f9438ac946dd 100644 --- a/test/e2e/specs/editor/various/navigable-toolbar.spec.js +++ b/test/e2e/specs/editor/various/navigable-toolbar.spec.js @@ -3,6 +3,12 @@ */ const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); +test.use( { + BlockToolbarUtils: async ( { page, pageUtils }, use ) => { + await use( new BlockToolbarUtils( { page, pageUtils } ) ); + }, +} ); + test.describe( 'Block Toolbar', () => { test.beforeEach( async ( { admin } ) => { await admin.createNewPost(); @@ -44,6 +50,67 @@ test.describe( 'Block Toolbar', () => { } ); expect( scrollTopBefore ).toBe( scrollTopAfter ); } ); + + test( 'can navigate to the block toolbar and back to block using the keyboard', async ( { + BlockToolbarUtils, + editor, + page, + pageUtils, + } ) => { + // Test navigating to block toolbar + await editor.insertBlock( { name: 'core/paragraph' } ); + await page.keyboard.type( 'Paragraph' ); + await BlockToolbarUtils.focusBlockToolbar(); + await BlockToolbarUtils.expectLabelToHaveFocus( 'Paragraph' ); + // // Navigate to Align Text + await page.keyboard.press( 'ArrowRight' ); + await BlockToolbarUtils.expectLabelToHaveFocus( 'Align text' ); + // // Open the dropdown + await page.keyboard.press( 'Enter' ); + await BlockToolbarUtils.expectLabelToHaveFocus( 'Align text left' ); + await page.keyboard.press( 'ArrowDown' ); + await BlockToolbarUtils.expectLabelToHaveFocus( + 'Align text center' + ); + await page.keyboard.press( 'Escape' ); + await BlockToolbarUtils.expectLabelToHaveFocus( 'Align text' ); + + // Navigate to the Bold item. Testing items via the fills within the block toolbar are especially important + await page.keyboard.press( 'ArrowRight' ); + await BlockToolbarUtils.expectLabelToHaveFocus( 'Bold' ); + + await BlockToolbarUtils.focusBlock(); + await BlockToolbarUtils.expectLabelToHaveFocus( + 'Block: Paragraph' + ); + + await BlockToolbarUtils.focusBlockToolbar(); + await BlockToolbarUtils.expectLabelToHaveFocus( 'Bold' ); + + await BlockToolbarUtils.focusBlock(); + + // Try selecting text and navigating to block toolbar + await pageUtils.pressKeys( 'Shift+ArrowLeft', { + times: 4, + delay: 50, + } ); + expect( + await editor.canvas + .locator( ':root' ) + .evaluate( () => window.getSelection().toString() ) + ).toBe( 'raph' ); + + // Go back to the toolbar and apply a formatting option + await BlockToolbarUtils.focusBlockToolbar(); + await BlockToolbarUtils.expectLabelToHaveFocus( 'Bold' ); + await page.keyboard.press( 'Enter' ); + // Should focus the selected text again + expect( + await editor.canvas + .locator( ':root' ) + .evaluate( () => window.getSelection().toString() ) + ).toBe( 'raph' ); + } ); } ); test( 'should focus with Shift+Tab', async ( { @@ -61,3 +128,31 @@ test.describe( 'Block Toolbar', () => { ).toBeFocused(); } ); } ); + +class BlockToolbarUtils { + constructor( { page, pageUtils } ) { + this.page = page; + this.pageUtils = pageUtils; + } + + async focusBlockToolbar() { + await this.pageUtils.pressKeys( 'alt+F10' ); + } + + async focusBlock() { + await this.pageUtils.pressKeys( 'Escape' ); + } + + async expectLabelToHaveFocus( label ) { + const ariaLabel = await this.page.evaluate( () => { + const { activeElement } = + document.activeElement.contentDocument ?? document; + return ( + activeElement.getAttribute( 'aria-label' ) || + activeElement.innerText + ); + } ); + + expect( ariaLabel ).toBe( label ); + } +} diff --git a/test/e2e/specs/editor/various/preview.spec.js b/test/e2e/specs/editor/various/preview.spec.js index cfec384adba9b..ea920270e093e 100644 --- a/test/e2e/specs/editor/various/preview.spec.js +++ b/test/e2e/specs/editor/various/preview.spec.js @@ -161,6 +161,7 @@ test.describe( 'Preview', () => { await editor.canvas .locator( 'role=textbox[name="Add title"i]' ) .type( 'Lorem' ); + await editor.openDocumentSettingsSidebar(); // Open the preview page. const previewPage = await editor.openPreviewPage( editorPage ); diff --git a/test/e2e/specs/editor/various/publish-button.spec.js b/test/e2e/specs/editor/various/publish-button.spec.js new file mode 100644 index 0000000000000..b1e4b07a28580 --- /dev/null +++ b/test/e2e/specs/editor/various/publish-button.spec.js @@ -0,0 +1,103 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +function defer() { + let resolve; + const deferred = new Promise( ( res ) => { + resolve = res; + } ); + deferred.resolve = resolve; + return deferred; +} + +test.describe( 'Post publish button', () => { + test( 'should be disabled when post is not saveable', async ( { + admin, + page, + } ) => { + await admin.createNewPost(); + await expect( + page + .getByRole( 'region', { name: 'Editor top bar' } ) + .getByRole( 'button', { name: 'Publish' } ) + ).toBeDisabled(); + } ); + + test( 'should be disabled when post is being saved', async ( { + admin, + editor, + page, + } ) => { + await admin.createNewPost(); + await editor.canvas + .getByRole( 'textbox', { + name: 'Add title', + } ) + .fill( 'Test post' ); + + const topBar = page.getByRole( 'region', { name: 'Editor top bar' } ); + await expect( + topBar.getByRole( 'button', { name: 'Publish' } ) + ).toBeEnabled(); + + const postId = new URL( page.url() ).searchParams.get( 'post' ); + const deferred = defer(); + + await page.route( + ( url ) => + url.searchParams.has( + 'rest_route', + encodeURIComponent( `/wp/v2/posts/${ postId }` ) + ), + async ( route ) => { + await deferred; + await route.continue(); + } + ); + + await topBar.getByRole( 'button', { name: 'Save draft' } ).click(); + await expect( + topBar.getByRole( 'button', { name: 'Publish' } ) + ).toBeDisabled(); + deferred.resolve(); + } ); + + test( 'should be disabled when metabox is being saved', async ( { + admin, + page, + requestUtils, + } ) => { + await requestUtils.activatePlugin( 'gutenberg-test-plugin-meta-box' ); + await admin.createNewPost(); + await page + .getByRole( 'textbox', { + name: 'Add title', + } ) + .fill( 'Test post' ); + + const topBar = page.getByRole( 'region', { name: 'Editor top bar' } ); + await expect( + topBar.getByRole( 'button', { name: 'Publish' } ) + ).toBeEnabled(); + + const deferred = defer(); + + await page.route( + ( url ) => url.searchParams.has( 'meta-box-loader', 1 ), + async ( route ) => { + await deferred; + await route.continue(); + } + ); + + await topBar.getByRole( 'button', { name: 'Save draft' } ).click(); + await expect( + topBar.getByRole( 'button', { name: 'Publish' } ) + ).toBeDisabled(); + deferred.resolve(); + + await requestUtils.deactivatePlugin( 'gutenberg-test-plugin-meta-box' ); + } ); +} ); diff --git a/test/native/jest.config.js b/test/native/jest.config.js index 7ecbf8036a03c..ad5c794ebbce8 100644 --- a/test/native/jest.config.js +++ b/test/native/jest.config.js @@ -18,6 +18,19 @@ const transpiledPackageNames = glob( 'packages/*/src/index.{js,ts}' ).map( ( fileName ) => fileName.split( '/' )[ 1 ] ); +// The following unit tests related to the `raw-handling` API will be enabled when addressing +// the various errors we encounter when running them in the native version. +// Reference: https://github.com/WordPress/gutenberg/issues/55652 +const RAW_HANDLING_UNSUPPORTED_UNIT_TESTS = [ + 'html-formatting-remover', + 'phrasing-content-reducer', + 'ms-list-converter', + 'figure-content-reducer', + 'special-comment-converter', + 'normalise-blocks', + 'image-corrector', +]; + module.exports = { rootDir: '../../', // Automatically clear mock calls and instances between every test. @@ -29,6 +42,10 @@ module.exports = { '/test/**/*.native.[jt]s?(x)', '/**/test/!(helper)*.native.[jt]s?(x)', '/packages/react-native-*/**/?(*.)+(spec|test).[jt]s?(x)', + // Enable `raw-handling` API unit tests to check jsdom patches. + `/packages/blocks/src/api/raw-handling/**/test/!(${ RAW_HANDLING_UNSUPPORTED_UNIT_TESTS.join( + '|' + ) }).[jt]s?(x)`, ], testPathIgnorePatterns: [ '/node_modules/', '/__device-tests__/' ], testEnvironmentOptions: { diff --git a/test/performance/config/performance-reporter.ts b/test/performance/config/performance-reporter.ts index decea4cb7a9a4..fa7cc90825c22 100644 --- a/test/performance/config/performance-reporter.ts +++ b/test/performance/config/performance-reporter.ts @@ -32,6 +32,7 @@ export interface WPRawPerformanceResults { inserterSearch: number[]; inserterHover: number[]; listViewOpen: number[]; + navigate: number[]; } export interface WPPerformanceResults { @@ -65,6 +66,7 @@ export interface WPPerformanceResults { listViewOpen?: number; minListViewOpen?: number; maxListViewOpen?: number; + navigate?: number; } /** @@ -108,6 +110,7 @@ export function curateResults( listViewOpen: average( results.listViewOpen ), minListViewOpen: minimum( results.listViewOpen ), maxListViewOpen: maximum( results.listViewOpen ), + navigate: median( results.navigate ), }; return ( diff --git a/test/performance/specs/site-editor.spec.js b/test/performance/specs/site-editor.spec.js index 28a1cbb0ecde2..bd1a5f0b87cc0 100644 --- a/test/performance/specs/site-editor.spec.js +++ b/test/performance/specs/site-editor.spec.js @@ -27,6 +27,7 @@ const results = { inserterHover: [], inserterSearch: [], listViewOpen: [], + navigate: [], }; test.describe( 'Site Editor Performance', () => { @@ -106,6 +107,7 @@ test.describe( 'Site Editor Performance', () => { } ); } } ); + test.describe( 'Typing', () => { let draftURL = null; @@ -187,6 +189,58 @@ test.describe( 'Site Editor Performance', () => { } } ); } ); + + test.describe( 'Navigating', () => { + test.beforeAll( async ( { requestUtils } ) => { + await requestUtils.activateTheme( 'twentytwentythree' ); + } ); + + test.afterAll( async ( { requestUtils } ) => { + await requestUtils.activateTheme( 'twentytwentyone' ); + } ); + + const iterations = 5; + for ( let i = 1; i <= iterations; i++ ) { + test( `Run the test (${ i } of ${ iterations })`, async ( { + admin, + page, + metrics, + } ) => { + await admin.visitSiteEditor( { + path: '/wp_template', + } ); + + // Wait for the loader overlay to disappear. This is necessary + // because the overlay is still visible for a while after the editor + // canvas is ready, and we don't want it to affect the typing + // timings. + await page + .locator( + // Spinner was used instead of the progress bar in an earlier version of the site editor. + '.edit-site-canvas-loader, .edit-site-canvas-spinner' + ) + .waitFor( { state: 'hidden' } ); + // Additional time to ensure the browser is completely idle. + // eslint-disable-next-line playwright/no-wait-for-timeout + + // Start tracing. + await metrics.startTracing(); + + await page + .getByRole( 'button', { name: 'Single Posts' } ) + .click(); + + // Stop tracing. + await metrics.stopTracing(); + + // Get the durations. + const [ mouseClickEvents ] = metrics.getClickEventDurations(); + + // Save the results. + results.navigate.push( mouseClickEvents[ 0 ] ); + } ); + } + } ); } ); /* eslint-enable playwright/no-conditional-in-test, playwright/expect-expect */