diff --git a/changelog.txt b/changelog.txt index 2356249f0b9f5e..92fa2690b15d68 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,316 @@ == Changelog == += 17.2.0-rc.1 = + + + +## Changelog + +### Features + +#### Modules API +- Interactivity API: Use modules instead of scripts in the frontend. ([56143](https://github.com/WordPress/gutenberg/pull/56143)) + + +### Enhancements + +- Add translator comments for strings containing date formats. ([56531](https://github.com/WordPress/gutenberg/pull/56531)) +- Block Settings: Only display parent block selector on small screens. ([56431](https://github.com/WordPress/gutenberg/pull/56431)) +- Block Theme Preview: Display the theme name on the activate button. ([55752](https://github.com/WordPress/gutenberg/pull/55752)) +- Core data revisions: Extend support to other post types. ([56353](https://github.com/WordPress/gutenberg/pull/56353)) +- Improve tooltip for parent blocks on the block toolbar. ([56146](https://github.com/WordPress/gutenberg/pull/56146)) +- Simplify template author token. ([56566](https://github.com/WordPress/gutenberg/pull/56566)) +- Style engine: Allow CSS var output for fontSize and fontFamily and update documentation. ([56528](https://github.com/WordPress/gutenberg/pull/56528)) +- Try: Change "Detach pattern" to "Detach". ([56323](https://github.com/WordPress/gutenberg/pull/56323)) +- useEntityRecord: Improve unit tests. ([56415](https://github.com/WordPress/gutenberg/pull/56415)) + +#### Components +- Add focus rings to focusable disabled buttons. ([56383](https://github.com/WordPress/gutenberg/pull/56383)) +- DropdownMenu V2 tweaks. ([56041](https://github.com/WordPress/gutenberg/pull/56041)) +- DropdownMenu V2: Add support for rendering in legacy popover slot. ([56342](https://github.com/WordPress/gutenberg/pull/56342)) +- FormToggle: Refine animation. ([56515](https://github.com/WordPress/gutenberg/pull/56515)) +- Slot: Add styles prop to bubblesVirtually version. ([56428](https://github.com/WordPress/gutenberg/pull/56428)) +- Tabs: Cleanup and improvements. ([56224](https://github.com/WordPress/gutenberg/pull/56224)) +- Try Ariakit Select for new CustomSelectControl component. ([55790](https://github.com/WordPress/gutenberg/pull/55790)) + +#### Data Views +- Data list view: Make filter row, table header, and pagination sticky. ([56157](https://github.com/WordPress/gutenberg/pull/56157)) +- Simplify dataviews view button. ([56485](https://github.com/WordPress/gutenberg/pull/56485)) +- Update data view menu item actions. ([56398](https://github.com/WordPress/gutenberg/pull/56398)) + +#### Global Styles +- Global style revisions: Redesign style revision items. ([55913](https://github.com/WordPress/gutenberg/pull/55913)) +- Global styles revisions: Migrate API call to getRevisions(). ([56349](https://github.com/WordPress/gutenberg/pull/56349)) +- Style Revisions: Remove style revisions dropdown menu. ([56454](https://github.com/WordPress/gutenberg/pull/56454)) + +#### Site Editor +- Add 'View site' action to 'Site updated' snackbar. ([52693](https://github.com/WordPress/gutenberg/pull/52693)) +- Add the Post Author component to the Page sidebar. ([56368](https://github.com/WordPress/gutenberg/pull/56368)) +- Redirect to main page menu if page record not found. ([56177](https://github.com/WordPress/gutenberg/pull/56177)) + +#### Block Editor +- Drag and drop: Allow dragging to the beginning and end of a document. ([56070](https://github.com/WordPress/gutenberg/pull/56070)) +- List View: Expand state if a block is dragged to within a collapsed block in the editor canvas. ([56493](https://github.com/WordPress/gutenberg/pull/56493)) + +#### Layout +- Add layout classes to legacy Group inner container. ([56130](https://github.com/WordPress/gutenberg/pull/56130)) +- Add setting to disable custom content size controls. ([56236](https://github.com/WordPress/gutenberg/pull/56236)) + +#### Patterns +- Small tweaks to CreatePatternModal. ([56016](https://github.com/WordPress/gutenberg/pull/56016)) +- Update Labels in Block Inserter (block patterns tab). ([55986](https://github.com/WordPress/gutenberg/pull/55986)) + +#### Icons +- Update trash icon. ([56569](https://github.com/WordPress/gutenberg/pull/56569)) + +#### Block Library +- Disable block renaming support for Nav Link block. ([56425](https://github.com/WordPress/gutenberg/pull/56425)) + +#### Distraction Free +- Add top toolbar to distraction free mode. ([56295](https://github.com/WordPress/gutenberg/pull/56295)) + +#### CSS & Styling +- Gallery Block: Use styled scrollbars for image captions. ([56252](https://github.com/WordPress/gutenberg/pull/56252)) + +#### Typography +- Font Library: Remove insecure properties. ([56230](https://github.com/WordPress/gutenberg/pull/56230)) + + +### New APIs + +- Revisions: Add new selectors to fetch entity revisions. ([54046](https://github.com/WordPress/gutenberg/pull/54046)) + +#### Interactivity API +- Migration to the new `store()` API. ([55459](https://github.com/WordPress/gutenberg/pull/55459)) + + +### Bug Fixes + +- Block Editor: Undeprecate the '__experimentalImageSizeControl' component. ([56414](https://github.com/WordPress/gutenberg/pull/56414)) +- Core data: Harmonize getRevision selector and resolver function signatures. ([56416](https://github.com/WordPress/gutenberg/pull/56416)) +- Editor styles: Scope without adding specificity. ([56564](https://github.com/WordPress/gutenberg/pull/56564)) +- Fix Restore Post title placeholder. ([56580](https://github.com/WordPress/gutenberg/pull/56580)) +- Post Schedule Panel: Remove text overflow ellipsis. ([56319](https://github.com/WordPress/gutenberg/pull/56319)) +- PostCSS style transformation: Fail gracefully instead of throwing an error. ([56093](https://github.com/WordPress/gutenberg/pull/56093)) +- Rich text: Pad multiple spaces through en/em replacement. ([56341](https://github.com/WordPress/gutenberg/pull/56341)) +- Site Editor Sidebar: Fix actions vertical alignment. ([56218](https://github.com/WordPress/gutenberg/pull/56218)) +- Site Editor: Add a fallback template showing the title and content for the post only mode. ([56509](https://github.com/WordPress/gutenberg/pull/56509)) +- useEntityRecord: Do not trigger REST API requests when disabled. ([56108](https://github.com/WordPress/gutenberg/pull/56108)) + +#### Block Library +- File block: Remove anchor tag when copy pasting to file name. ([56508](https://github.com/WordPress/gutenberg/pull/56508)) +- Fix label of columns inspector panel. ([56647](https://github.com/WordPress/gutenberg/pull/56647)) +- Post Template: Fix incorrect offset query. ([56440](https://github.com/WordPress/gutenberg/pull/56440)) + +#### Block Editor +- (RichText)(Workaround)(17.1.x) Fallback to a string arg in `collapseWhiteSpace()` if `value` is not a string. ([56570](https://github.com/WordPress/gutenberg/pull/56570)) +- Cover block: Pass dropZoneElement reference to fix dragging within cover block area. ([56312](https://github.com/WordPress/gutenberg/pull/56312)) +- useMovingAnimation: Clear translate3d rule when animation is finished. ([56410](https://github.com/WordPress/gutenberg/pull/56410)) + +#### Components +- Design Tools: Fix last ToolsPanelItem styling. ([56536](https://github.com/WordPress/gutenberg/pull/56536)) +- Fix FormTokenField suggestions broken scrollbar when `__experimentalExpandOnFocus` is defined. ([56426](https://github.com/WordPress/gutenberg/pull/56426)) +- Tabs: Fix flaky unit tests. ([55950](https://github.com/WordPress/gutenberg/pull/55950)) + +#### Global Styles +- Additional CSS: Fix on change validation. ([56434](https://github.com/WordPress/gutenberg/pull/56434)) +- Global styles revisions: Update isResolving flag. ([56491](https://github.com/WordPress/gutenberg/pull/56491)) +- Spacing: Fix block error if spacing unit array empty in theme.json. ([56306](https://github.com/WordPress/gutenberg/pull/56306)) + +#### CSS & Styling +- Reduce specificity of default Cover text color styles. ([56411](https://github.com/WordPress/gutenberg/pull/56411)) +- Restore Post Title visual styles in Code View mode. ([56582](https://github.com/WordPress/gutenberg/pull/56582)) + +#### Saving +- Editor: Reinstate anonymous callback for saved post state. ([56529](https://github.com/WordPress/gutenberg/pull/56529)) + +#### Post Editor +- Save post button: Avoid extra re-renders when enablng/disabling tooltip. ([56502](https://github.com/WordPress/gutenberg/pull/56502)) + +#### Plugin +- Update Readme.txt tested up to 6.4. ([56427](https://github.com/WordPress/gutenberg/pull/56427)) + +#### Site Editor +- Fix template resolution for templates assigned as home page. ([56418](https://github.com/WordPress/gutenberg/pull/56418)) + +#### Patterns +- Fix issue with template in replace template screen. ([56407](https://github.com/WordPress/gutenberg/pull/56407)) + +#### Layout +- Fix issue where layout classnames are injected for blocks without layout support. ([56187](https://github.com/WordPress/gutenberg/pull/56187)) + +#### Typography +- Font Library: Fix fonts not displaying correctly. ([55393](https://github.com/WordPress/gutenberg/pull/55393)) + +#### Colors +- Duotone: Backport from Core to fix filters in classic themes. ([54778](https://github.com/WordPress/gutenberg/pull/54778)) + + +### Accessibility + +- Migrating `StyleBook` to use updated `Composite` implementation. ([55344](https://github.com/WordPress/gutenberg/pull/55344)) + +#### Data Views +- DataViews: Make disabled pagination buttons focusable. ([56422](https://github.com/WordPress/gutenberg/pull/56422)) + +#### Block Library +- Image Block: Enable image block to be selected correctly when clicked. ([56043](https://github.com/WordPress/gutenberg/pull/56043)) + +#### Post Editor +- Tooltip: Don't render buttons tooltip when show button text labels is enabled. ([55842](https://github.com/WordPress/gutenberg/pull/55842)) + +#### Components +- Improve `Button` saving state accessibility. ([55547](https://github.com/WordPress/gutenberg/pull/55547)) + +#### Patterns +- Fix focus loss after converting to a synced pattern. ([55473](https://github.com/WordPress/gutenberg/pull/55473)) + + +### Performance + +- Avoid calling postcss when not needed. ([56601](https://github.com/WordPress/gutenberg/pull/56601)) +- Block Editor: Optimize 'Connections' inspector controls. ([56443](https://github.com/WordPress/gutenberg/pull/56443)) + +#### Global Styles +- Make search more responsive for block type list. ([56139](https://github.com/WordPress/gutenberg/pull/56139)) + + +### Experiments + +#### Data Views +- DataViews: Document `view.layout`. ([56637](https://github.com/WordPress/gutenberg/pull/56637)) +- DataViews: Extract common constants to file. ([56251](https://github.com/WordPress/gutenberg/pull/56251)) +- DataViews: Rename `InFilter` component to `FilterSummary`. ([56506](https://github.com/WordPress/gutenberg/pull/56506)) +- DataViews: Scope names of V2 UI components. ([56503](https://github.com/WordPress/gutenberg/pull/56503)) +- DataViews: Update field API to generate filters based on type. ([55996](https://github.com/WordPress/gutenberg/pull/55996)) +- DataViews: Update filter component. ([56110](https://github.com/WordPress/gutenberg/pull/56110)) +- Dataviews: Add confirmation step before deleting a page. ([56504](https://github.com/WordPress/gutenberg/pull/56504)) +- Dataviews: Add preview and grid view in templates list. ([56382](https://github.com/WordPress/gutenberg/pull/56382)) +- Dataviews: Grid layout refinements. ([56441](https://github.com/WordPress/gutenberg/pull/56441)) +- Dataviews: Remove link from author. ([56467](https://github.com/WordPress/gutenberg/pull/56467)) +- Dataviews: Update item actions in grid view. ([56501](https://github.com/WordPress/gutenberg/pull/56501)) +- Fix data view menu item radius. ([56395](https://github.com/WordPress/gutenberg/pull/56395)) + +#### Post Editor +- Render html in post titles in visual mode and edit HTML in post title in code view. ([54718](https://github.com/WordPress/gutenberg/pull/54718)) + + +### Documentation + +- Add the attributes definition page to the create block tutorial of the platform documentation. ([56429](https://github.com/WordPress/gutenberg/pull/56429)) +- Add the transforms page to the create block tutorial of the platform documentation. ([56559](https://github.com/WordPress/gutenberg/pull/56559)) +- Add thee block supports page to the create block tutorial of the framework docs. ([56483](https://github.com/WordPress/gutenberg/pull/56483)) +- Added clarifications and examples to "Get started with wp-scripts". ([56298](https://github.com/WordPress/gutenberg/pull/56298)) +- Block Editor: Fix typo in `URLInput`'s `onKeyDown` prop documentation. ([56322](https://github.com/WordPress/gutenberg/pull/56322)) +- Bring back non-JS tabs in block editor handbook. ([56561](https://github.com/WordPress/gutenberg/pull/56561)) +- Docs: Fix incorrect build script description in script package. ([56332](https://github.com/WordPress/gutenberg/pull/56332)) +- Docs: Fundamentals of Block Development - File structure of a block. ([56551](https://github.com/WordPress/gutenberg/pull/56551)) +- Docs: Fundamentals of Block Development - Registration of a block. ([56334](https://github.com/WordPress/gutenberg/pull/56334)) +- Docs: Fundamentals of Block Development - The block wrapper. ([56596](https://github.com/WordPress/gutenberg/pull/56596)) +- Docs: Fundamentals of Block Development - Working with Javascript in the Block Editor. ([56553](https://github.com/WordPress/gutenberg/pull/56553)) +- Docs: Fundamentals of Block Development - block.json. ([56435](https://github.com/WordPress/gutenberg/pull/56435)) +- Docs: Improve downloadBlob example. ([56225](https://github.com/WordPress/gutenberg/pull/56225)) +- Documentation - Block Editor Handbook - Add end user documentation about Block Editor as a resource on the Landing Page of the Block Editor Handbook. ([49854](https://github.com/WordPress/gutenberg/pull/49854)) +- Fix overly complex code example in ComboboxControl readme. ([56365](https://github.com/WordPress/gutenberg/pull/56365)) +- Fix version in useSetting deprecation notice. ([56377](https://github.com/WordPress/gutenberg/pull/56377)) +- Fundamentals block development - landing and first pages. ([56584](https://github.com/WordPress/gutenberg/pull/56584)) +- Fundamentals of Block Development - fix save definition. ([56605](https://github.com/WordPress/gutenberg/pull/56605)) +- Link preview image to live example using WordPress Playground. ([56292](https://github.com/WordPress/gutenberg/pull/56292)) +- NavigableContainers: Fix doc typo in onKeyDown prop. ([56352](https://github.com/WordPress/gutenberg/pull/56352)) +- Release docs: Add new section about troubleshooting the release. ([56436](https://github.com/WordPress/gutenberg/pull/56436)) +- Remove all {% codetabs %} instances and any vanilla JS references. ([56121](https://github.com/WordPress/gutenberg/pull/56121)) +- Simplify code example in ToggleControl component readme. ([56389](https://github.com/WordPress/gutenberg/pull/56389)) +- Text and Heading: Improve documentation around default values and truncation logic. ([56518](https://github.com/WordPress/gutenberg/pull/56518)) +- Theme JSON schema: Add heading/button key to color definition. ([55674](https://github.com/WordPress/gutenberg/pull/55674)) +- Update for 6.4.1 for versions in WP. ([56216](https://github.com/WordPress/gutenberg/pull/56216)) +- Update references to the gutenberg-examples repo to the new block-development-examples. ([56119](https://github.com/WordPress/gutenberg/pull/56119)) +- Update template name in `create-block` command. ([56281](https://github.com/WordPress/gutenberg/pull/56281)) +- Update webpack options for wp-scripts in README.md. ([56314](https://github.com/WordPress/gutenberg/pull/56314)) +- `BoxControl`: Update story and refactor to Typescript. ([56462](https://github.com/WordPress/gutenberg/pull/56462)) + + +### Code Quality + +- Blocks pkg: Remove 'browser' dependencies. ([56433](https://github.com/WordPress/gutenberg/pull/56433)) +- DataViews: Code Quality remove some unused props from action. ([56477](https://github.com/WordPress/gutenberg/pull/56477)) +- Editor: Move the template focus modes to the editor store. ([56472](https://github.com/WordPress/gutenberg/pull/56472)) +- Extract a PostPanelRow component from the different sidebar panels. ([56238](https://github.com/WordPress/gutenberg/pull/56238)) +- Interactivity API: Add missing changelog entry for the new `store()` API. ([56611](https://github.com/WordPress/gutenberg/pull/56611)) +- Migrating block editor `BlockPatternsList` component. ([56210](https://github.com/WordPress/gutenberg/pull/56210)) +- Move the DisableNonContentBlocks component to the editor package. ([56423](https://github.com/WordPress/gutenberg/pull/56423)) +- Post Schedule Panel: Fix Sass deprecation warning for division. ([56412](https://github.com/WordPress/gutenberg/pull/56412)) +- Remove compatibility layer for WP 6.2. ([56464](https://github.com/WordPress/gutenberg/pull/56464)) +- Unify the PostSchedule component between site and post editors. ([56196](https://github.com/WordPress/gutenberg/pull/56196)) +- Update: Refactor useAddedBy to use authorText and originalSource fields. ([56568](https://github.com/WordPress/gutenberg/pull/56568)) + +#### Block Library +- Add align support to the image block - alternative. ([55954](https://github.com/WordPress/gutenberg/pull/55954)) +- Backmerge block renaming fixes/refactors from 6.4 branch into Gutenberg trunk. ([56386](https://github.com/WordPress/gutenberg/pull/56386)) +- Pattern placeholder: Remove duplicate 'useDispatch' hook. ([56397](https://github.com/WordPress/gutenberg/pull/56397)) + +#### Components +- Remove incorrect version from deprecated `__nextHasNoMarginBottom` prop of `AnglePickerControl` Component. ([56336](https://github.com/WordPress/gutenberg/pull/56336)) +- Revert "DropdownMenu V2: Add support for rendering in legacy popover slot". ([56484](https://github.com/WordPress/gutenberg/pull/56484)) + +#### Data Views +- Dataviews: Ensure items and fields are using a unique id. ([56366](https://github.com/WordPress/gutenberg/pull/56366)) + +#### Block Editor +- useInnerBlocksProps: Stabilise dropZoneElement prop. ([56313](https://github.com/WordPress/gutenberg/pull/56313)) + +#### Design Tools +- Fix: Theme.json font settings in unit test. ([56309](https://github.com/WordPress/gutenberg/pull/56309)) + + +### Tools + +- Workflows: Update 'days-before-stale' for flaky test report issues. ([56585](https://github.com/WordPress/gutenberg/pull/56585)) +- scripts: Update `jest-dev-server` to v9. ([56552](https://github.com/WordPress/gutenberg/pull/56552)) + +#### Testing +- Dataviews: Add first end-to-end tests. ([56634](https://github.com/WordPress/gutenberg/pull/56634)) +- Migrate 'align hook' end-to-end tests to Playwright. ([56480](https://github.com/WordPress/gutenberg/pull/56480)) +- Migrate 'block directory' end-to-end tests to Playwright. ([56593](https://github.com/WordPress/gutenberg/pull/56593)) +- Migrate 'block icons' end-to-end tests to Playwright. ([56610](https://github.com/WordPress/gutenberg/pull/56610)) +- Migrate 'custom taxonomies' end-to-end test to Playwright. ([56486](https://github.com/WordPress/gutenberg/pull/56486)) +- Migrate 'sidebar permalink' end-to-end tests to Playwright. ([56253](https://github.com/WordPress/gutenberg/pull/56253)) +- Migrate Is Typing Test to Playwright. ([56616](https://github.com/WordPress/gutenberg/pull/56616)) +- Page spec: Merging create page and toggle preview tests. ([56129](https://github.com/WordPress/gutenberg/pull/56129)) +- Playwright Utils: Fix the method of getting post ID in 'publishPost'. ([56421](https://github.com/WordPress/gutenberg/pull/56421)) +- end-to-end tests: Merge Puppeteer into single job, split Playwright further. ([56363](https://github.com/WordPress/gutenberg/pull/56363)) + +#### Build Tooling +- Create block: Update `interactive-template` to the new `store()` API. ([56613](https://github.com/WordPress/gutenberg/pull/56613)) + + +### Security + +- WP_Theme_JSON_Gutenberg: Add nested indexed array schema sanitization. ([56447](https://github.com/WordPress/gutenberg/pull/56447)) + + +### Various + +- Add: Author text and original source to wp_template_part. ([56567](https://github.com/WordPress/gutenberg/pull/56567)) +- Migrating `BlockPatternSetup` to use updated `Composite` implementation. ([55425](https://github.com/WordPress/gutenberg/pull/55425)) +- Migrating `InserterListbox` to use updated Composite implementation. ([56246](https://github.com/WordPress/gutenberg/pull/56246)) + +#### Data Views +- Dataviews: All Templates: Add filters to template author. ([56338](https://github.com/WordPress/gutenberg/pull/56338)) +- Dataviews: All templates: Add: Sorting to template author and add author_text to the rest API. ([56333](https://github.com/WordPress/gutenberg/pull/56333)) + +#### HTML API +- Backport updates from Core. ([56578](https://github.com/WordPress/gutenberg/pull/56578)) + + + + +## Contributors + +The following contributors merged PRs in this release: + +@aaronrobertshaw @afercia @andrewhayward @andrewserong @annezazu @apeatling @arthur791004 @bph @brookewp @chad1008 @chiilog @ciampo @DAreRodz @dmsnell @draganescu @ellatrix @fabiankaegy @flootr @fluiddot @fullofcaffeine @geriux @getdave @glendaviesnz @jameskoster @jasmussen @jeryj @jffng @jorgefilipecosta @juanmaguitar @kevin940726 @luisherranz @MaggieCabrera @Mamaduka @matiasbenedetto @megane9988 @NekoJonez @ntsekouras @oandregal @ramonjd @richtabor @ryanwelcher @SavPhill @Soean @t-hamano @talldan @tellthemachines @youknowriad @zaguiini + + = 17.1.3 = diff --git a/docs/getting-started/fundamentals/README.md b/docs/getting-started/fundamentals/README.md new file mode 100644 index 00000000000000..26fc88981348b8 --- /dev/null +++ b/docs/getting-started/fundamentals/README.md @@ -0,0 +1,11 @@ +# Fundamentals of Block Development + +This section provides an introduction to the most relevant concepts in Block Development. + +In this section, you will learn: + +1. [**File structure of a block**](https://developer.wordpress.org/block-editor/getting-started/fundamentals/file-structure-of-a-block) - The purpose of each one of the types of files available for a block, the relationships between them, and their role in the output of the block. +1. [**`block.json`**](https://developer.wordpress.org/block-editor/getting-started/fundamentals/block-json) - How a block is defined using its `block.json` metadata and some relevant properties of this file. +1. [**Registration of a block**](https://developer.wordpress.org/block-editor/getting-started/fundamentals/registration-of-a-block) - How a block is registered in both the server and the client. +1. [**Block wrapper**](https://developer.wordpress.org/block-editor/getting-started/fundamentals/block-wrapper) - How to set proper attributes to the block's markup wrapper. +1. [**Javascript in the Block Editor**](https://developer.wordpress.org/block-editor/getting-started/fundamentals/javascript-in-the-block-editor) - How to work with Javascript for the Block Editor. \ No newline at end of file diff --git a/docs/getting-started/fundamentals/block-json.md b/docs/getting-started/fundamentals/block-json.md new file mode 100644 index 00000000000000..3d65a8f016914e --- /dev/null +++ b/docs/getting-started/fundamentals/block-json.md @@ -0,0 +1,115 @@ +# block.json + +The `block.json` file simplifies the processs of defining and registering a block by using the same block's definition in JSON format to register the block in both the server and the client. + +[![Open block.json diagram in excalidraw](https://developer.wordpress.org/files/2023/11/block-json.png)](https://excalidraw.com/#json=v1GrIkGsYGKv8P14irBy6,Yy0vl8q7DTTL2VsH5Ww27A "Open block.json diagram in excalidraw") + +
+Click here to see a full block example and check its block.json +
+ +Besides simplifying a block's registration, using a `block.json` has [several benefits](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-metadata/#benefits-using-the-metadata-file), including improved performance and development. + +At [**Metadata in block.json**](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-metadata/) you can find a detailed explanation of all the properties you can set in a `block.json` for a block. With these properties you can define things such as: + +- Basic metadata of the block +- Files for the block's behavior, style, or output +- Data Storage in the Block +- Setting UI panels for the block + +## Basic metadata of the block + +Through properties of the `block.json`, we can define how the block will be uniquely identified, how it can be found, and the info displayed for the block in the Block Editor. Some of these properties are: + +- `apiVersion`: the version of [the API](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-api-versions/) used by the block (current version is 2). +- `name`: a unique identifier for a block, including a namespace. +- `title`: a display title for a block. +- `category`: a block category for the block in the Inserter panel. +- `icon`: a [Dashicon](https://developer.wordpress.org/resource/dashicons) slug or a custom SVG icon. +- `description`: a short description visible in the block inspector. +- `keywords`: to locate the block in the inserter. +- `textdomain`: the plugin text-domain (important for things such as translations). + +## Files for the block's behavior, output, or style + +The `editorScript` and `editorStyle` properties allow defining Javascript and CSS files to be enqueued and loaded **only in the editor**. + +The `script` and `style` properties allow the definition of Javascript and CSS files to be enqueued and loaded **in both the editor and the front end**. + +The `viewScript` property allow us to define the Javascript file or files to be enqueued and loaded **only in the front end**. + +All these properties (`editorScript`, `editorStyle`, `script` `style`,`viewScript`) accept as a value a path for the file, a handle registered with `wp_register_script` or `wp_register_style`, or an array with a mix of both. Paths values in `block.json` are prefixed with `file:`. + +The `render` property ([introduced on WordPress 6.1](https://make.wordpress.org/core/2022/10/12/block-api-changes-in-wordpress-6-1/)) sets the path of a `.php` template file that will render the markup returned to the front end. This only method will be used to return the markup for the block on request only if `$render_callback` function has not been passed to the `register_block_type` function. + +## Data Storage in the Block with `attributes` + +The [`attributes` property](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-attributes/) allows a block to declare "variables" that store data or content for the block. + +_Example: Attributes as defined in block.json_ +```json +"attributes": { + "fallbackCurrentYear": { + "type": "string" + }, + "showStartingYear": { + "type": "boolean" + }, + "startingYear": { + "type": "string" + } +}, +``` +By default `attributes` are serialized and stored in the block's delimiter but this [can be configured](https://developer.wordpress.org/news/2023/09/understanding-block-attributes/). + +_Example: Atributes stored in the Markup representation of the block_ +```html + + +x +``` + +These attributes are passed to the React component `Edit`(to display in the Block Editor) and the `save` function (to return the markup saved to the DB) of the block, and to any server-side render definition for the block (see `render` prop above). + +The `Edit` component receives exclusively the capability of updating the attributes via the `setAttributes` function. + +_See how the attributes are passed to the [`Edit` component](https://github.com/WordPress/block-development-examples/blob/trunk/plugins/copyright-date-block-09aac3/src/edit.js), [the `save` function](https://github.com/WordPress/block-development-examples/blob/trunk/plugins/copyright-date-block-09aac3/src/save.js) and [the `render.php`](https://github.com/WordPress/block-development-examples/blob/trunk/plugins/copyright-date-block-09aac3/src/render.php) in this [full block example](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/copyright-date-block-09aac3) of the code above_ + +
+Check the attributes reference page for full info about the Attributes API. +
+ +[![Open Attributes diagram in excalidraw](https://developer.wordpress.org/files/2023/11/attributes.png)](https://excalidraw.com/#json=pSgCZy8q9GbH7r0oz2fL1,MFCLd6ddQHqi_UqNp5ZSgg "Open Attributes diagram in excalidraw") + + +## Enable UI settings panels for the block with `supports` + +The `supports` property allows a block to declare support for certain features, enabling users to customize specific settings (like colors or margins) from the Settings Sidebar. + +_Example: Supports as defined in block.json_ + +```json +"supports": { + "color": { + "text": true, + "link": true, + "background": true + } +} +``` + +The use of `supports` generates a set of properties that need to be manually added to the wrapping element of the block so they're properly stored as part of the block data. + +_Example: Supports custom settings stored in the Markup representation of the block_ + +```html + +

Hello World

+ +``` + +_See the [full block example](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/block-supports-6aa4dd) of the [code above](https://github.com/WordPress/block-development-examples/blob/trunk/plugins/block-supports-6aa4dd/src/block.json)_ + +
+Check the supports reference page for full info about the Supports API. +
diff --git a/docs/getting-started/fundamentals/block-wrapper.md b/docs/getting-started/fundamentals/block-wrapper.md new file mode 100644 index 00000000000000..7dce10fc80f89f --- /dev/null +++ b/docs/getting-started/fundamentals/block-wrapper.md @@ -0,0 +1,114 @@ +# The block wrapper + +Each block's markup is wrapped by a container HTML tag that needs to have the proper attributes to fully work in the Block Editor and to reflect the proper block's style settings when rendered in the Block Editor and the front end. As developers, we have full control over the block's markup, and WordPress provides the tools to add the attributes that need to exist on the wrapper to our block's markup. + +Ensuring proper attributes to the block wrapper is especially important when using custom styling or features like `supports`. + +
+The use of supports generates a set of properties that need to be manually added to the wrapping element of the block so they're properly stored as part of the block data +
+ +A block can have three sets of markup defined, each one of them with a specific target and purpose: + +- The one for the **Block Editor**, defined through a `edit` React component passed to `registerBlockType` when registering the block in the client. +- The one used to **save the block in the DB**, defined through a `save` function passed to `registerBlockType` when registering the block in the client. + - This markup will be returned to the front end on request if no dynamic render has been defined for the block. +- The one used to **dynamically render the markup of the block** returned to the front end on request, defined through the `render_callback` on `register_block_type` or the `render` PHP file in `block.json` + - If defined, this server-side generated markup will be returned to the front end, ignoring the markup stored in DB. + +For the React component `edit` and the `save` function, the block wrapper element should be a native DOM element (like `
`) or a React component that forwards any additional props to native DOM elements. Using a or component, for instance, would be invalid. + + +## The Edit component's markup + +The `useBlockProps()` hook available on the `@wordpress/block-editor` allows passing the required attributes for the Block Editor to the `edit` block's outer wrapper. + +Among other things, the `useBlockProps()` hook takes care of including in this wrapper: +- An `id` for the block's markup +- Some accesibility and `data-` attributes +- Classes and inline styles reflecting custom settings, which include by default: + - The `wp-block` class + - A class that contains the name of the block with its namespace + +For example, for the following piece of code of a block's registration in the client... + +```js +const Edit = () =>

Hello World - Block Editor

; + +registerBlockType( ..., { + edit: Edit +} ); +``` +_(see the [code above](https://github.com/WordPress/block-development-examples/blob/trunk/plugins/minimal-block-ca6eda/src/index.js) in [an example](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/minimal-block-ca6eda))_ + +...the markup of the block in the Block Editor could look like this: +```html +

Hello World - Block Editor

+``` + +Any additional classes and attributes for the `Edit` component of the block should be passed as an argument of `useBlockProps` (see [example](https://github.com/WordPress/block-development-examples/blob/trunk/plugins/stylesheets-79a4c3/src/edit.js)). When you add `support` for any feature, they get added to the object returned by the `useBlockProps` hook. + + +## The Save component's markup + +When saving the markup in the DB, it’s important to add the block props returned by `useBlockProps.save()` to the wrapper element of your block. `useBlockProps.save()` ensures that the block class name is rendered properly in addition to any HTML attribute injected by the block supports API. + +For example, for the following piece of code of a block's registration in the client that defines the markup desired for the DB (and returned to the front end by default)... + +```js +const Edit = () =>

Hello World - Block Editor

; +const save = () =>

Hello World - Frontend

; + +registerBlockType( ..., { + edit: Edit, + save, +} ); +``` + +_(see the [code above](https://github.com/WordPress/block-development-examples/blob/trunk/plugins/minimal-block-ca6eda/src/index.js) in [an example](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/minimal-block-ca6eda))_ + + +...the markup of the block in the front end could look like this: +```html +

Hello World – Frontend

+``` + +Any additional classes and attributes for the `save` function of the block should be passed as an argument of `useBlockProps.save()` (see [example](https://github.com/WordPress/block-development-examples/blob/trunk/plugins/stylesheets-79a4c3/src/save.js)). + +When you add `support` for any feature, the proper classes get added to the object returned by the `useBlockProps.save()` hook. + +```html +

Hello World

+``` + +_(check the [example](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/block-supports-6aa4dd) that generated the HTML above in the front end)_ + +## The server-side render markup + +Any markup in the server-side render definition for the block can use the [`get_block_wrapper_attributes()`](https://developer.wordpress.org/reference/functions/get_block_wrapper_attributes/) function to generate the string of attributes required to reflect the block settings (see [example](https://github.com/WordPress/block-development-examples/blob/f68640f42d993f0866d1879f67c73910285ca114/plugins/block-dynamic-rendering-64756b/src/render.php#L11)). + +```php +

> + +

+``` \ No newline at end of file diff --git a/docs/getting-started/fundamentals-block-development/file-structure-of-a-block.md b/docs/getting-started/fundamentals/file-structure-of-a-block.md similarity index 92% rename from docs/getting-started/fundamentals-block-development/file-structure-of-a-block.md rename to docs/getting-started/fundamentals/file-structure-of-a-block.md index dac8dd6c338091..130483ae5af70f 100644 --- a/docs/getting-started/fundamentals-block-development/file-structure-of-a-block.md +++ b/docs/getting-started/fundamentals/file-structure-of-a-block.md @@ -1,8 +1,8 @@ # File structure of a block -It is recommended to **register blocks within plugins** to ensure they stay available when a theme gets switched. With the `create-block` tool you can quickly scaffold the structure of the files required to create a plugin that registers a block. +It is recommended to **register blocks within plugins** to ensure they stay available when a theme gets switched. With the [`create-block` tool](https://developer.wordpress.org/block-editor/getting-started/devenv/get-started-with-create-block/) you can quickly scaffold the structure of the files required to create a plugin that registers a block. -The files generated by this tool are a good reference of the files that can be involved in the definition and registration of a block. +The files generated by `create-block` are a good reference of the files that can be involved in the definition and registration of a block. [![Open File Structure of a Block Diagram in excalidraw](https://developer.wordpress.org/files/2023/11/file-structure-block.png)](https://excalidraw.com/#json=YYpeR-kY1ZMhFKVZxGhMi,mVZewfwNAh_oL-7bj4gmdw "Open File Structure of a Block Diagram in excalidraw") @@ -47,7 +47,7 @@ The `edit.js` commonly gets used to contain the React component that gets used i ### `save.js` -The `save.js` is similar to the `edit.js` file in that it exports a single React component. This component generates the static HTML markup that gets saved to the Database. +The `save.js` exports the function that returns the static HTML markup that gets saved to the Database. ### `style.(css|scss|sass)` diff --git a/docs/getting-started/fundamentals-block-development/javascript-in-the-block-editor.md b/docs/getting-started/fundamentals/javascript-in-the-block-editor.md similarity index 90% rename from docs/getting-started/fundamentals-block-development/javascript-in-the-block-editor.md rename to docs/getting-started/fundamentals/javascript-in-the-block-editor.md index 0ca88e1447437e..73c6a6c56e6328 100644 --- a/docs/getting-started/fundamentals-block-development/javascript-in-the-block-editor.md +++ b/docs/getting-started/fundamentals/javascript-in-the-block-editor.md @@ -30,7 +30,7 @@ With the [proper `package.json` scripts](https://developer.wordpress.org/block-e Using Javascript without a build process may be another good option for code developments with few requirements (especially those not requiring JSX). -Without a build process, you access the methods directly from the `wp` global object and must enqueue the script manually. [WordPress Javascript](https://developer.wordpress.org/block-editor/reference-guides/packages/) packages](https://developer.wordpress.org/block-editor/reference-guides/packages/) can be accessed through the `wp` [global variable](https://developer.mozilla.org/en-US/docs/Glossary/Global_variable) but every script that wants to use them through this `wp` object is responsible for adding [the handle of that package](https://developer.wordpress.org/block-editor/contributors/code/scripts/) to the dependency array when registered. +Without a build process, you access the methods directly from the `wp` global object and must enqueue the script manually. [WordPress Javascript packages](https://developer.wordpress.org/block-editor/reference-guides/packages/) can be accessed through the `wp` [global variable](https://developer.mozilla.org/en-US/docs/Glossary/Global_variable) but every script that wants to use them through this `wp` object is responsible for adding [the handle of that package](https://developer.wordpress.org/block-editor/contributors/code/scripts/) to the dependency array when registered. So, for example if a script wants to register a block variation using the `registerBlockVariation` method out of the ["blocks" package](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-blocks/), the `wp-blocks` handle would need to get added to the dependency array to ensure that `wp.blocks.registerBlockVariation` is defined when the script tries to access it (see [example](https://github.com/wptrainingteam/block-theme-examples/blob/master/example-block-variation/functions.php)). @@ -42,10 +42,10 @@ Use [`enqueue_block_editor_assets`](https://developer.wordpress.org/reference/ho ## Additional resources +- [Get started with wp-scripts](https://developer.wordpress.org/block-editor/getting-started/devenv/get-started-with-wp-scripts/) +- [Enqueueing assets in the Editor](https://developer.wordpress.org/block-editor/how-to-guides/enqueueing-assets-in-the-editor/) +- [Wordpress Packages handles](https://developer.wordpress.org/block-editor/contributors/code/scripts/) - [Javascript Reference](https://developer.mozilla.org/en-US/docs/Web/JavaScript) | MDN Web Docs - [block-development-examples](https://github.com/WordPress/block-development-examples) | GitHub repository - [block-theme-examples](https://github.com/wptrainingteam/block-theme-examples) | GitHub repository -- [Get started with wp-scripts](https://developer.wordpress.org/block-editor/getting-started/devenv/get-started-with-wp-scripts/) | Block Editor Handbook -- [Enqueueing assets in the Editor](https://developer.wordpress.org/block-editor/how-to-guides/enqueueing-assets-in-the-editor/) | Block Editor Handbook -- [Wordpress Packages handles](https://developer.wordpress.org/block-editor/contributors/code/scripts/) | Block Editor Handbook - [How webpack and WordPress packages interact](https://developer.wordpress.org/news/2023/04/how-webpack-and-wordpress-packages-interact/) | Developer Blog diff --git a/docs/getting-started/fundamentals-block-development/registration-of-a-block.md b/docs/getting-started/fundamentals/registration-of-a-block.md similarity index 94% rename from docs/getting-started/fundamentals-block-development/registration-of-a-block.md rename to docs/getting-started/fundamentals/registration-of-a-block.md index 3e34f1368e8c41..7cc8e6bcbe8b06 100644 --- a/docs/getting-started/fundamentals-block-development/registration-of-a-block.md +++ b/docs/getting-started/fundamentals/registration-of-a-block.md @@ -70,7 +70,7 @@ The content of block.json (or any other .json file) ca The client-side block settings object passed as a second parameter include two properties that are especially relevant: - `edit`: The React component that gets used in the editor for our block. -- `save`: The React component that generates the static HTML markup that gets saved to the Database. +- `save`: The function that returns the static HTML markup that gets saved to the Database. `registerBlockType` returns the registered block type (`WPBlock`) on success or `undefined` on failure. @@ -78,15 +78,15 @@ The client-side block settings object passed as a second parameter include two p ```js import { registerBlockType } from '@wordpress/blocks'; +import { useBlockProps } from '@wordpress/block-editor'; import metadata from './block.json'; +const Edit = () =>

Hello World - Block Editor

; +const save = () =>

Hello World - Frontend

; + registerBlockType( metadata.name, { - edit() { - return

Hello World - Block Editor

; - }, - save() { - return

Hello World - Frontend

; - }, + edit: Edit, + save, } ); ``` _See the [code above](https://github.com/WordPress/block-development-examples/blob/trunk/plugins/minimal-block-ca6eda/src/index.js) in [an example](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/minimal-block-ca6eda)_ diff --git a/docs/manifest.json b/docs/manifest.json index 5906743512062c..3ab4cefb2b533c 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -95,6 +95,42 @@ "markdown_source": "../docs/getting-started/create-block/submitting-to-block-directory.md", "parent": "create-block" }, + { + "title": "Fundamentals of Block Development", + "slug": "fundamentals", + "markdown_source": "../docs/getting-started/fundamentals/README.md", + "parent": "getting-started" + }, + { + "title": "File structure of a block", + "slug": "file-structure-of-a-block", + "markdown_source": "../docs/getting-started/fundamentals/file-structure-of-a-block.md", + "parent": "fundamentals" + }, + { + "title": "block.json", + "slug": "block-json", + "markdown_source": "../docs/getting-started/fundamentals/block-json.md", + "parent": "fundamentals" + }, + { + "title": "Registration of a block", + "slug": "registration-of-a-block", + "markdown_source": "../docs/getting-started/fundamentals/registration-of-a-block.md", + "parent": "fundamentals" + }, + { + "title": "The block wrapper", + "slug": "block-wrapper", + "markdown_source": "../docs/getting-started/fundamentals/block-wrapper.md", + "parent": "fundamentals" + }, + { + "title": "Working with Javascript for the Block Editor", + "slug": "javascript-in-the-block-editor", + "markdown_source": "../docs/getting-started/fundamentals/javascript-in-the-block-editor.md", + "parent": "fundamentals" + }, { "title": "Glossary", "slug": "glossary", diff --git a/docs/reference-guides/block-api/block-metadata.md b/docs/reference-guides/block-api/block-metadata.md index a91dfd747fa24b..edc61d138128e6 100644 --- a/docs/reference-guides/block-api/block-metadata.md +++ b/docs/reference-guides/block-api/block-metadata.md @@ -78,7 +78,7 @@ Development is improved by using a defined schema definition file. Supported edi ```
-Check Registration of a block to learn more about how to register a block using its metadata. +Check Registration of a block to learn more about how to register a block using its metadata.
## Block API diff --git a/docs/reference-guides/core-blocks.md b/docs/reference-guides/core-blocks.md index 4f42550ba4cfbc..dd7ef824aa6b0c 100644 --- a/docs/reference-guides/core-blocks.md +++ b/docs/reference-guides/core-blocks.md @@ -378,7 +378,7 @@ Insert an image to make a visual statement. ([Source](https://github.com/WordPre - **Name:** core/image - **Category:** media -- **Supports:** align (center, full, left, right, wide), anchor, color (~~background~~, ~~text~~), filter (duotone) +- **Supports:** align (center, full, left, right, wide), anchor, color (~~background~~, ~~text~~), filter (duotone), interactivity - **Attributes:** alt, aspectRatio, caption, height, href, id, lightbox, linkClass, linkDestination, linkTarget, rel, scale, sizeSlug, title, url, width ## Latest Comments diff --git a/docs/reference-guides/data/data-core.md b/docs/reference-guides/data/data-core.md index b2a75638ace9fe..b80703dcc67b18 100644 --- a/docs/reference-guides/data/data-core.md +++ b/docs/reference-guides/data/data-core.md @@ -741,7 +741,7 @@ _Returns_ ### receiveRevisions -Returns an action object used in signalling that revisions have been received. +Action triggered to receive revision items. _Parameters_ @@ -753,10 +753,6 @@ _Parameters_ - _invalidateCache_ `?boolean`: Should invalidate query caches. - _meta_ `?Object`: Meta information about pagination. -_Returns_ - -- `Object`: Action object. - ### receiveThemeSupports > **Deprecated** since WP 5.9, this is not useful anymore, use the selector direclty. diff --git a/docs/toc.json b/docs/toc.json index 8a29d2d4f10aff..91017ce69643c3 100644 --- a/docs/toc.json +++ b/docs/toc.json @@ -46,6 +46,25 @@ } ] }, + { + "docs/getting-started/fundamentals/README.md": [ + { + "docs/getting-started/fundamentals/file-structure-of-a-block.md": [] + }, + { + "docs/getting-started/fundamentals/block-json.md": [] + }, + { + "docs/getting-started/fundamentals/registration-of-a-block.md": [] + }, + { + "docs/getting-started/fundamentals/block-wrapper.md": [] + }, + { + "docs/getting-started/fundamentals/javascript-in-the-block-editor.md": [] + } + ] + }, { "docs/getting-started/glossary.md": [] }, { "docs/getting-started/faq.md": [] } ] diff --git a/lib/class-wp-theme-json-gutenberg.php b/lib/class-wp-theme-json-gutenberg.php index 646c7abc59eafe..9311001f2edd14 100644 --- a/lib/class-wp-theme-json-gutenberg.php +++ b/lib/class-wp-theme-json-gutenberg.php @@ -426,6 +426,31 @@ class WP_Theme_JSON_Gutenberg { ), ); + const FONT_FAMILY_SCHEMA = array( + array( + 'fontFamily' => null, + 'name' => null, + 'slug' => null, + 'fontFace' => array( + array( + 'ascentOverride' => null, + 'descentOverride' => null, + 'fontDisplay' => null, + 'fontFamily' => null, + 'fontFeatureSettings' => null, + 'fontStyle' => null, + 'fontStretch' => null, + 'fontVariationSettings' => null, + 'fontWeight' => null, + 'lineGapOverride' => null, + 'sizeAdjust' => null, + 'src' => null, + 'unicodeRange' => null, + ), + ), + ), + ); + /** * The valid properties under the styles key. * @@ -550,6 +575,52 @@ class WP_Theme_JSON_Gutenberg { 'typography' => 'typography', ); + /** + * Return the input schema at the root and per origin. + * + * @since 6.5.0 + * + * @param array $schema The base schema. + * @return array The schema at the root and per origin. + * + * Example: + * schema_in_root_and_per_origin( + * array( + * 'fontFamily' => null, + * 'slug' => null, + * ) + * ) + * + * Returns: + * array( + * 'fontFamily' => null, + * 'slug' => null, + * 'default' => array( + * 'fontFamily' => null, + * 'slug' => null, + * ), + * 'blocks' => array( + * 'fontFamily' => null, + * 'slug' => null, + * ), + * 'theme' => array( + * 'fontFamily' => null, + * 'slug' => null, + * ), + * 'custom' => array( + * 'fontFamily' => null, + * 'slug' => null, + * ), + * ) + */ + protected static function schema_in_root_and_per_origin( $schema ) { + $schema_in_root_and_per_origin = $schema; + foreach ( static::VALID_ORIGINS as $origin ) { + $schema_in_root_and_per_origin[ $origin ] = $schema; + } + return $schema_in_root_and_per_origin; + } + /** * Returns a class name by an element name. * @@ -791,11 +862,12 @@ protected static function sanitize( $input, $valid_block_names, $valid_element_n $schema_styles_blocks[ $block ]['variations'] = $schema_styles_variations; } - $schema['styles'] = static::VALID_STYLES; - $schema['styles']['blocks'] = $schema_styles_blocks; - $schema['styles']['elements'] = $schema_styles_elements; - $schema['settings'] = static::VALID_SETTINGS; - $schema['settings']['blocks'] = $schema_settings_blocks; + $schema['styles'] = static::VALID_STYLES; + $schema['styles']['blocks'] = $schema_styles_blocks; + $schema['styles']['elements'] = $schema_styles_elements; + $schema['settings'] = static::VALID_SETTINGS; + $schema['settings']['blocks'] = $schema_settings_blocks; + $schema['settings']['typography']['fontFamilies'] = static::schema_in_root_and_per_origin( static::FONT_FAMILY_SCHEMA ); // Remove anything that's not present in the schema. foreach ( array( 'styles', 'settings' ) as $subtree ) { @@ -967,18 +1039,39 @@ protected static function get_blocks_metadata() { * @return array The modified $tree. */ protected static function remove_keys_not_in_schema( $tree, $schema ) { - $tree = array_intersect_key( $tree, $schema ); + if ( ! is_array( $tree ) ) { + return $tree; + } - foreach ( $schema as $key => $data ) { - if ( ! isset( $tree[ $key ] ) ) { + foreach ( $tree as $key => $value ) { + // Remove keys not in the schema or with null/empty values. + if ( ! array_key_exists( $key, $schema ) ) { + unset( $tree[ $key ] ); continue; } - if ( is_array( $schema[ $key ] ) && is_array( $tree[ $key ] ) ) { - $tree[ $key ] = static::remove_keys_not_in_schema( $tree[ $key ], $schema[ $key ] ); + // Check if the value is an array and requires further processing. + if ( is_array( $value ) && is_array( $schema[ $key ] ) ) { + // Determine if it is an associative or indexed array. + $schema_is_assoc = self::is_assoc( $value ); + + if ( $schema_is_assoc ) { + // If associative, process as a single object. + $tree[ $key ] = self::remove_keys_not_in_schema( $value, $schema[ $key ] ); - if ( empty( $tree[ $key ] ) ) { - unset( $tree[ $key ] ); + if ( empty( $tree[ $key ] ) ) { + unset( $tree[ $key ] ); + } + } else { + // If indexed, process each item in the array. + foreach ( $value as $item_key => $item_value ) { + if ( isset( $schema[ $key ][0] ) && is_array( $schema[ $key ][0] ) ) { + $tree[ $key ][ $item_key ] = self::remove_keys_not_in_schema( $item_value, $schema[ $key ][0] ); + } else { + // If the schema does not define a further structure, keep the value as is. + $tree[ $key ][ $item_key ] = $item_value; + } + } } } elseif ( is_array( $schema[ $key ] ) && ! is_array( $tree[ $key ] ) ) { unset( $tree[ $key ] ); @@ -988,6 +1081,20 @@ protected static function remove_keys_not_in_schema( $tree, $schema ) { return $tree; } + /** + * Checks if the given array is associative. + * + * @since 6.5.0 + * @param array $data The array to check. + * @return bool True if the array is associative, false otherwise. + */ + protected static function is_assoc( $data ) { + if ( array() === $data ) { + return false; + } + return array_keys( $data ) !== range( 0, count( $data ) - 1 ); + } + /** * Returns the existing settings for each block. * 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 509d2c1a2c9abd..e22b4fb17b902e 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 @@ -116,7 +116,7 @@ * * Example: * - * if ( $tags->next_tag( array( 'class' => 'wp-group-block' ) ) ) { + * if ( $tags->next_tag( array( 'class_name' => 'wp-group-block' ) ) ) { * $tags->set_attribute( 'title', 'This groups the contained content.' ); * $tags->remove_attribute( 'data-test-id' ); * } @@ -2031,8 +2031,8 @@ public function set_attribute( $name, $value ) { * * @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? + * @todo As the only regex pattern maybe we should take it out? + * Are Unicode patterns available broadly in Core? */ if ( preg_match( '~[' . diff --git a/lib/compat/wordpress-6.4/html-api/class-wp-html-processor.php b/lib/compat/wordpress-6.4/html-api/class-wp-html-processor.php index e53e64c80e2e02..d1c8b9e82c708a 100644 --- a/lib/compat/wordpress-6.4/html-api/class-wp-html-processor.php +++ b/lib/compat/wordpress-6.4/html-api/class-wp-html-processor.php @@ -103,12 +103,16 @@ * * The following list specifies the HTML tags that _are_ supported: * + * - Containers: ADDRESS, BLOCKQUOTE, DETAILS, DIALOG, DIV, FOOTER, HEADER, MAIN, MENU, SPAN, SUMMARY. + * - Form elements: BUTTON, FIELDSET, SEARCH. + * - Formatting elements: B, BIG, CODE, EM, FONT, I, SMALL, STRIKE, STRONG, TT, U. + * - Heading elements: HGROUP. * - Links: A. - * - The formatting elements: B, BIG, CODE, EM, FONT, I, SMALL, STRIKE, STRONG, TT, U. - * - Containers: DIV, FIGCAPTION, FIGURE, SPAN. - * - Form elements: BUTTON. + * - Lists: DL. + * - Media elements: FIGCAPTION, FIGURE, IMG. * - Paragraph: P. - * - Void elements: IMG. + * - Sectioning elements: ARTICLE, ASIDE, NAV, SECTION + * - Deprecated elements: CENTER, DIR * * ### Supported markup * @@ -346,7 +350,7 @@ public function get_last_error() { /** * Finds the next tag matching the $query. * - * @TODO: Support matching the class name and tag name. + * @todo Support matching the class name and tag name. * * @since 6.4.0 * @@ -555,9 +559,9 @@ public function step( $node_to_process = self::PROCESS_NEXT_NODE ) { * Breadcrumbs start at the outermost parent and descend toward the matched element. * They always include the entire path from the root HTML node to the matched element. * - * @TODO: It could be more efficient to expose a generator-based version of this function - * to avoid creating the array copy on tag iteration. If this is done, it would likely - * be more useful to walk up the stack when yielding instead of starting at the top. + * @todo It could be more efficient to expose a generator-based version of this function + * to avoid creating the array copy on tag iteration. If this is done, it would likely + * be more useful to walk up the stack when yielding instead of starting at the top. * * Example * @@ -625,11 +629,29 @@ private function step_in_body() { * > "fieldset", "figcaption", "figure", "footer", "header", "hgroup", * > "main", "menu", "nav", "ol", "p", "search", "section", "summary", "ul" */ + case '+ADDRESS': + case '+ARTICLE': + case '+ASIDE': case '+BLOCKQUOTE': + case '+CENTER': + case '+DETAILS': + case '+DIALOG': + case '+DIR': case '+DIV': + case '+DL': + case '+FIELDSET': case '+FIGCAPTION': case '+FIGURE': + case '+FOOTER': + case '+HEADER': + case '+HGROUP': + case '+MAIN': + case '+MENU': + case '+NAV': case '+P': + case '+SEARCH': + case '+SECTION': + case '+SUMMARY': if ( $this->state->stack_of_open_elements->has_p_in_button_scope() ) { $this->close_a_p_element(); } @@ -643,11 +665,29 @@ private function step_in_body() { * > "figcaption", "figure", "footer", "header", "hgroup", "listing", "main", * > "menu", "nav", "ol", "pre", "search", "section", "summary", "ul" */ + case '-ADDRESS': + case '-ARTICLE': + case '-ASIDE': case '-BLOCKQUOTE': case '-BUTTON': + case '-CENTER': + case '-DETAILS': + case '-DIALOG': + case '-DIR': case '-DIV': + case '-DL': + case '-FIELDSET': case '-FIGCAPTION': case '-FIGURE': + case '-FOOTER': + case '-HEADER': + case '-HGROUP': + case '-MAIN': + case '-MENU': + case '-NAV': + case '-SEARCH': + case '-SECTION': + case '-SUMMARY': if ( ! $this->state->stack_of_open_elements->has_element_in_scope( $tag_name ) ) { // @TODO: Report parse error. // Ignore the token. diff --git a/lib/compat/wordpress-6.5/class-wp-navigation-block-renderer.php b/lib/compat/wordpress-6.5/class-wp-navigation-block-renderer.php index e2eb4e10414fe8..9c2314ebe6890c 100644 --- a/lib/compat/wordpress-6.5/class-wp-navigation-block-renderer.php +++ b/lib/compat/wordpress-6.5/class-wp-navigation-block-renderer.php @@ -429,25 +429,25 @@ private static function get_responsive_container_markup( $attributes, $inner_blo $close_button_directives = ''; if ( $should_load_view_script ) { $open_button_directives = ' - data-wp-on--click="actions.core.navigation.openMenuOnClick" - data-wp-on--keydown="actions.core.navigation.handleMenuKeydown" + data-wp-on--click="actions.openMenuOnClick" + data-wp-on--keydown="actions.handleMenuKeydown" '; $responsive_container_directives = ' - data-wp-class--has-modal-open="selectors.core.navigation.isMenuOpen" - data-wp-class--is-menu-open="selectors.core.navigation.isMenuOpen" - data-wp-effect="effects.core.navigation.initMenu" - data-wp-on--keydown="actions.core.navigation.handleMenuKeydown" - data-wp-on--focusout="actions.core.navigation.handleMenuFocusout" + data-wp-class--has-modal-open="state.isMenuOpen" + data-wp-class--is-menu-open="state.isMenuOpen" + data-wp-watch="callbacks.initMenu" + data-wp-on--keydown="actions.handleMenuKeydown" + data-wp-on--focusout="actions.handleMenuFocusout" tabindex="-1" '; $responsive_dialog_directives = ' - data-wp-bind--aria-modal="selectors.core.navigation.ariaModal" - data-wp-bind--aria-label="selectors.core.navigation.ariaLabel" - data-wp-bind--role="selectors.core.navigation.roleAttribute" - data-wp-effect="effects.core.navigation.focusFirstElement" + data-wp-bind--aria-modal="state.ariaModal" + data-wp-bind--aria-label="state.ariaLabel" + data-wp-bind--role="state.roleAttribute" + data-wp-watch="callbacks.focusFirstElement" '; $close_button_directives = ' - data-wp-on--click="actions.core.navigation.closeMenuOnClick" + data-wp-on--click="actions.closeMenuOnClick" '; } @@ -521,19 +521,15 @@ private static function get_nav_element_directives( $should_load_view_script ) { // When adding to this array be mindful of security concerns. $nav_element_context = wp_json_encode( array( - 'core' => array( - 'navigation' => array( - 'overlayOpenedBy' => array(), - 'type' => 'overlay', - 'roleAttribute' => '', - 'ariaLabel' => __( 'Menu' ), - ), - ), + 'overlayOpenedBy' => array(), + 'type' => 'overlay', + 'roleAttribute' => '', + 'ariaLabel' => __( 'Menu' ), ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP ); return ' - data-wp-interactive + data-wp-interactive=\'{"namespace":"core/navigation"}\' data-wp-context=\'' . $nav_element_context . '\' '; } @@ -547,20 +543,28 @@ private static function get_nav_element_directives( $should_load_view_script ) { */ private static function handle_view_script_loading( $attributes, $block, $inner_blocks ) { $should_load_view_script = static::should_load_view_script( $attributes, $inner_blocks ); + $is_gutenberg_plugin = defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN; + $view_js_file = 'wp-block-navigation-view'; + $script_handles = $block->block_type->view_script_handles; - $view_js_file = 'wp-block-navigation-view'; - - // If the script already exists, there is no point in removing it from viewScript. - if ( ! wp_script_is( $view_js_file ) ) { - $script_handles = $block->block_type->view_script_handles; - - // If the script is not needed, and it is still in the `view_script_handles`, remove it. - if ( ! $should_load_view_script && in_array( $view_js_file, $script_handles, true ) ) { - $block->block_type->view_script_handles = array_diff( $script_handles, array( $view_js_file ) ); + if ( $is_gutenberg_plugin ) { + if ( $should_load_view_script ) { + gutenberg_enqueue_module( '@wordpress/block-library/navigation-block' ); } - // If the script is needed, but it was previously removed, add it again. - if ( $should_load_view_script && ! in_array( $view_js_file, $script_handles, true ) ) { - $block->block_type->view_script_handles = array_merge( $script_handles, array( $view_js_file ) ); + // Remove the view script because we are using the module. + $block->block_type->view_script_handles = array_diff( $script_handles, array( $view_js_file ) ); + } else { + // If the script already exists, there is no point in removing it from viewScript. + if ( ! wp_script_is( $view_js_file ) ) { + + // If the script is not needed, and it is still in the `view_script_handles`, remove it. + if ( ! $should_load_view_script && in_array( $view_js_file, $script_handles, true ) ) { + $block->block_type->view_script_handles = array_diff( $script_handles, array( $view_js_file ) ); + } + // If the script is needed, but it was previously removed, add it again. + if ( $should_load_view_script && ! in_array( $view_js_file, $script_handles, true ) ) { + $block->block_type->view_script_handles = array_merge( $script_handles, array( $view_js_file ) ); + } } } } diff --git a/lib/experimental/interactivity-api/class-wp-interactivity-store.php b/lib/experimental/interactivity-api/class-wp-interactivity-store.php index 89cb58700554a9..c53701b14e8aff 100644 --- a/lib/experimental/interactivity-api/class-wp-interactivity-store.php +++ b/lib/experimental/interactivity-api/class-wp-interactivity-store.php @@ -62,7 +62,7 @@ public static function render() { return; } echo sprintf( - '', + '', wp_json_encode( self::$store, JSON_HEX_TAG | JSON_HEX_AMP ) ); } diff --git a/lib/experimental/interactivity-api/modules.php b/lib/experimental/interactivity-api/modules.php new file mode 100644 index 00000000000000..02785a152ca1fa --- /dev/null +++ b/lib/experimental/interactivity-api/modules.php @@ -0,0 +1,33 @@ + 'defer', + ) + ); +} + +add_action( 'wp_enqueue_scripts', 'gutenberg_register_interactivity_module' ); diff --git a/lib/experimental/interactivity-api/scripts.php b/lib/experimental/interactivity-api/scripts.php deleted file mode 100644 index ed1fca85500701..00000000000000 --- a/lib/experimental/interactivity-api/scripts.php +++ /dev/null @@ -1,40 +0,0 @@ -=' ); - if ( $supports_defer ) { - // Defer execution of @wordpress/interactivity package but continue loading in head. - wp_script_add_data( 'wp-interactivity', 'strategy', 'defer' ); - wp_script_add_data( 'wp-interactivity', 'group', 0 ); - } else { - // Move the @wordpress/interactivity package to the footer. - wp_script_add_data( 'wp-interactivity', 'group', 1 ); - } - - // Move all the view scripts of the interactive blocks to the footer. - $registered_blocks = \WP_Block_Type_Registry::get_instance()->get_all_registered(); - foreach ( array_values( $registered_blocks ) as $block ) { - if ( isset( $block->supports['interactivity'] ) && $block->supports['interactivity'] ) { - foreach ( $block->view_script_handles as $handle ) { - // Note that all block view scripts are already made defer by default. - wp_script_add_data( $handle, 'group', $supports_defer ? 0 : 1 ); - } - } - } -} -add_action( 'wp_enqueue_scripts', 'gutenberg_interactivity_move_interactive_scripts_to_the_footer', 11 ); diff --git a/lib/experimental/modules/class-gutenberg-modules.php b/lib/experimental/modules/class-gutenberg-modules.php new file mode 100644 index 00000000000000..ca74d863043ee6 --- /dev/null +++ b/lib/experimental/modules/class-gutenberg-modules.php @@ -0,0 +1,195 @@ + isset( $dependencies['static'] ) || isset( $dependencies['dynamic'] ) ? $dependencies['static'] ?? array() : $dependencies, + 'dynamic' => isset( $dependencies['dynamic'] ) ? $dependencies['dynamic'] : array(), + ); + + self::$registered[ $module_identifier ] = array( + 'src' => $src, + 'version' => $version, + 'dependencies' => $deps, + ); + } + } + + /** + * Enqueues a module in the page. + * + * @param string $module_identifier The identifier of the module. + */ + public static function enqueue( $module_identifier ) { + // Add the module to the queue if it's not already there. + if ( ! in_array( $module_identifier, self::$enqueued, true ) ) { + self::$enqueued[] = $module_identifier; + } + } + + /** + * Returns the import map array. + * + * @return array Associative array with 'imports' key mapping to an array of module identifiers and their respective source strings. + */ + public static function get_import_map() { + $imports = array(); + foreach ( self::get_dependencies( self::$enqueued, array( 'static', 'dynamic' ) ) as $module_identifier => $module ) { + $imports[ $module_identifier ] = $module['src'] . self::get_version_query_string( $module['version'] ); + } + return array( 'imports' => $imports ); + } + + /** + * Prints the import map. + */ + public static function print_import_map() { + $import_map = self::get_import_map(); + if ( ! empty( $import_map['imports'] ) ) { + echo ''; + } + } + + /** + * Prints all the enqueued modules using HTML; diff --git a/packages/e2e-tests/plugins/interactive-blocks/store-tag/view.js b/packages/e2e-tests/plugins/interactive-blocks/store-tag/view.js index 140cab6463137f..0faa9625cedd78 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/store-tag/view.js +++ b/packages/e2e-tests/plugins/interactive-blocks/store-tag/view.js @@ -1,24 +1,24 @@ -( ( { wp } ) => { - /** - * WordPress dependencies - */ - const { store } = wp.interactivity; +/** + * WordPress dependencies + */ +import { store } from '@wordpress/interactivity'; - store( { - state: { - counter: { - // `value` is defined in the server. - double: ( { state } ) => state.counter.value * 2, - clicks: 0, +const { state } = store( 'store-tag', { + state: { + counter: { + // `value` is defined in the server. + get double() { + return state.counter.value * 2; }, + clicks: 0, }, - actions: { - counter: { - increment: ( { state } ) => { - state.counter.value += 1; - state.counter.clicks += 1; - }, + }, + actions: { + counter: { + increment() { + state.counter.value += 1; + state.counter.clicks += 1; }, }, - } ); -} )( window ); + }, +} ); diff --git a/packages/e2e-tests/plugins/interactive-blocks/tovdom-islands/render.php b/packages/e2e-tests/plugins/interactive-blocks/tovdom-islands/render.php index a3ebb7a87424e4..7b1bc6513977b8 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/tovdom-islands/render.php +++ b/packages/e2e-tests/plugins/interactive-blocks/tovdom-islands/render.php @@ -5,7 +5,9 @@ * @package gutenberg-test-interactive-blocks */ +gutenberg_enqueue_module( 'tovdom-islands-view' ); ?> +
@@ -13,7 +15,7 @@
-
+
This should not be shown because it is inside an island. @@ -21,7 +23,7 @@
-
+
-
-
+
+
-
+
-
+
{ - const { store, directive, createElement } = wp.interactivity; +/** + * WordPress dependencies + */ +import { store, directive, createElement as h } from '@wordpress/interactivity'; - // Fake `data-wp-show-mock` directive to test when things are removed from the - // DOM. Replace with `data-wp-show` when it's ready. - directive( - 'show-mock', - ( { - directives: { - "show-mock": { default: showMock }, - }, - element, - evaluate, - } ) => { - if ( ! evaluate( showMock ) ) - element.props.children = - createElement( "template", null, element.props.children ); +// Fake `data-wp-show-mock` directive to test when things are removed from the +// DOM. Replace with `data-wp-show` when it's ready. +directive( + 'show-mock', + ( { directives: { 'show-mock': showMock }, element, evaluate } ) => { + const entry = showMock.find( ( { suffix } ) => suffix === 'default' ); + + if ( ! evaluate( entry ) ) { + element.props.children = h( + 'template', + null, + element.props.children + ); } - ); + } +); - store( { - state: { - falseValue: false, - }, - } ); -} )( window ); +store( 'tovdom-islands', { + state: { + falseValue: false, + }, +} ); diff --git a/packages/e2e-tests/plugins/interactive-blocks/tovdom/render.php b/packages/e2e-tests/plugins/interactive-blocks/tovdom/render.php index 952a4f6c0a455d..309b42a5829359 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/tovdom/render.php +++ b/packages/e2e-tests/plugins/interactive-blocks/tovdom/render.php @@ -8,9 +8,11 @@ $plugin_url = plugin_dir_url( __DIR__ ); $src_proc_ins = $plugin_url . 'tovdom/processing-instructions.js'; $src_cdata = $plugin_url . 'tovdom/cdata.js'; + +gutenberg_enqueue_module( 'tovdom-view' ); ?> -
+
diff --git a/packages/e2e-tests/plugins/interactive-blocks/tovdom/view.js b/packages/e2e-tests/plugins/interactive-blocks/tovdom/view.js index 734ccbd801bb1e..75987cf19c5c74 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/tovdom/view.js +++ b/packages/e2e-tests/plugins/interactive-blocks/tovdom/view.js @@ -1,5 +1,6 @@ -( ( { wp } ) => { - const { store } = wp.interactivity; +/** + * WordPress dependencies + */ +import { store } from '@wordpress/interactivity'; - store( {} ); -} )( window ); +store( 'tovdom', {} ); diff --git a/packages/e2e-tests/specs/editor/plugins/__snapshots__/block-directory-add.test.js.snap b/packages/e2e-tests/specs/editor/plugins/__snapshots__/block-directory-add.test.js.snap deleted file mode 100644 index 2c06020e52c787..00000000000000 --- a/packages/e2e-tests/specs/editor/plugins/__snapshots__/block-directory-add.test.js.snap +++ /dev/null @@ -1,3 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`adding blocks from block directory Should be able to add (the first) block. 1`] = `""`; diff --git a/packages/e2e-tests/specs/editor/plugins/block-directory-add.test.js b/packages/e2e-tests/specs/editor/plugins/block-directory-add.test.js deleted file mode 100644 index 2e969d17915924..00000000000000 --- a/packages/e2e-tests/specs/editor/plugins/block-directory-add.test.js +++ /dev/null @@ -1,205 +0,0 @@ -/** - * WordPress dependencies - */ -import { - createNewPost, - searchForBlock, - insertBlockDirectoryBlock, - setUpResponseMocking, - getEditedPostContent, - createJSONResponse, -} from '@wordpress/e2e-test-utils'; - -const BLOCK1_NAME = 'block-directory-test-block/main-block'; - -// Urls to mock. -const SEARCH_URLS = [ - '/wp/v2/block-directory/search', - `rest_route=${ encodeURIComponent( '/wp/v2/block-directory/search' ) }`, -]; - -const BLOCK_TYPE_URLS = [ - `/wp/v2/block-types/${ BLOCK1_NAME }`, - `rest_route=${ encodeURIComponent( - `/wp/v2/block-types/${ BLOCK1_NAME }` - ) }`, -]; - -const INSTALL_URLS = [ - '/wp/v2/plugins', - `rest_route=${ encodeURIComponent( '/wp/v2/plugins' ) }`, -]; - -// Example Blocks. -const MOCK_BLOCK1 = { - name: BLOCK1_NAME, - title: 'Block Directory Test Block', - description: 'This plugin is useful for the block.', - id: 'block-directory-test-block', - rating: 0, - rating_count: 0, - active_installs: 0, - author_block_rating: 0, - author_block_count: 1, - author: 'No Author', - icon: 'block-default', - assets: [ - 'https://fake_url.com/block.js', // We will mock this. - ], - humanized_updated: '5 months ago', - links: {}, -}; - -const MOCK_INSTALLED_BLOCK_PLUGIN_DETAILS = { - plugin: 'block-directory-test-block', - status: 'active', - name: 'Block Directory', - plugin_uri: '', - author: 'No Author', - author_uri: '', - description: { - raw: 'This plugin is useful for the block.', - rendered: 'This plugin is useful for the block.', - }, - version: '1.0', - network_only: false, - requires_wp: '', - requires_php: '', - text_domain: 'block-directory-test-block', - _links: { - self: [ - { - href: '', - }, - ], - }, -}; - -const MOCK_BLOCK2 = { - ...MOCK_BLOCK1, - name: 'block-directory-test-block/secondary-block', - title: 'Block Directory Test Block - Pt Deux', - id: 'block-directory-test-secondary-block', -}; - -// Block that will be registered. -const block = `( function() { - var registerBlockType = wp.blocks.registerBlockType; - var el = wp.element.createElement; - - registerBlockType( '${ MOCK_BLOCK1.name }', { - title: 'Test Block for Block Directory', - icon: 'hammer', - category: 'text', - attributes: {}, - edit: function( props ) { - return el( 'p', null, 'Test Copy' ); - }, - save: function() { - return null; - }, - } ); -} )();`; - -const MOCK_EMPTY_RESPONSES = [ - { - match: ( request ) => - matchUrl( request.url(), SEARCH_URLS ) && - request.method() === 'GET', - onRequestMatch: createJSONResponse( [] ), - }, -]; - -const MOCK_BLOCKS_RESPONSES = [ - { - // Mock response for search with the block. - match: ( request ) => - matchUrl( request.url(), SEARCH_URLS ) && - request.method() === 'GET', - onRequestMatch: createJSONResponse( [ MOCK_BLOCK1, MOCK_BLOCK2 ] ), - }, - { - // Mock response for block type. - match: ( request ) => matchUrl( request.url(), BLOCK_TYPE_URLS ), - onRequestMatch: createJSONResponse( {} ), - }, - { - // Mock response for install. - match: ( request ) => matchUrl( request.url(), INSTALL_URLS ), - onRequestMatch: createJSONResponse( - MOCK_INSTALLED_BLOCK_PLUGIN_DETAILS - ), - }, - { - // Mock the response for the js asset once it gets injected. - match: ( request ) => request.url().includes( MOCK_BLOCK1.assets[ 0 ] ), - onRequestMatch: createResponse( - Buffer.from( block, 'utf8' ), - 'application/javascript; charset=utf-8' - ), - }, - { - // Mock the post-new page as requested via apiFetch for determining new CSS/JS assets. - match: ( request ) => request.url().includes( '/post-new.php' ), - onRequestMatch: createResponse( - ``, - 'text/html; charset=UTF-8' - ), - }, -]; - -function getResponseObject( obj, contentType ) { - return { - status: 200, - contentType, - body: obj, - }; -} - -function createResponse( mockResponse, contentType ) { - return async ( request ) => - request.respond( getResponseObject( mockResponse, contentType ) ); -} - -const matchUrl = ( reqUrl, urls ) => { - return urls.some( ( el ) => reqUrl.indexOf( el ) >= 0 ); -}; - -describe( 'adding blocks from block directory', () => { - beforeAll( async () => { - await createNewPost(); - } ); - - it( 'Should show an empty state when no plugin is found.', async () => { - // Be super weird so there won't be a matching block installed. - const impossibleBlockName = '@#$@@Dsdsdfw2#$@'; - - // Return an empty list of plugins. - await setUpResponseMocking( MOCK_EMPTY_RESPONSES ); - - // Search for the block via the inserter. - await searchForBlock( impossibleBlockName ); - - const selectorContent = await page.evaluate( - () => - document.querySelector( '.block-editor-inserter__main-area' ) - .innerHTML - ); - expect( selectorContent ).toContain( - 'block-editor-inserter__no-results' - ); - } ); - - it( 'Should be able to add (the first) block.', async () => { - // Setup our mocks. - await setUpResponseMocking( MOCK_BLOCKS_RESPONSES ); - - // Search for the block via the inserter. - await insertBlockDirectoryBlock( MOCK_BLOCK1.title ); - - await page.waitForSelector( `div[data-type="${ MOCK_BLOCK1.name }"]` ); - - // The block will auto select and get added, make sure we see it in the content. - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); -} ); diff --git a/packages/e2e-tests/specs/editor/plugins/block-icons.test.js b/packages/e2e-tests/specs/editor/plugins/block-icons.test.js deleted file mode 100644 index d70cf0e615753f..00000000000000 --- a/packages/e2e-tests/specs/editor/plugins/block-icons.test.js +++ /dev/null @@ -1,170 +0,0 @@ -/** - * WordPress dependencies - */ -import { - activatePlugin, - createNewPost, - deactivatePlugin, - insertBlock, - pressKeyWithModifier, - searchForBlock, - openDocumentSettingsSidebar, -} from '@wordpress/e2e-test-utils'; - -const INSERTER_BUTTON_SELECTOR = - '.block-editor-inserter__main-area .block-editor-block-types-list__item'; -const INSERTER_ICON_WRAPPER_SELECTOR = `${ INSERTER_BUTTON_SELECTOR } .block-editor-block-types-list__item-icon`; -const INSERTER_ICON_SELECTOR = `${ INSERTER_BUTTON_SELECTOR } .block-editor-block-icon`; -const INSPECTOR_ICON_SELECTOR = '.edit-post-sidebar .block-editor-block-icon'; - -async function getInnerHTML( selector ) { - return await page.$eval( selector, ( element ) => element.innerHTML ); -} - -async function getBackgroundColor( selector ) { - return await page.$eval( selector, ( element ) => { - return window.getComputedStyle( element ).backgroundColor; - } ); -} - -async function getColor( selector ) { - return await page.$eval( selector, ( element ) => { - return window.getComputedStyle( element ).color; - } ); -} - -async function getFirstInserterIcon() { - return await getInnerHTML( INSERTER_ICON_SELECTOR ); -} - -async function selectFirstBlock() { - await pressKeyWithModifier( 'access', 'o' ); - const navButtons = await page.$$( - '.block-editor-list-view-block-select-button' - ); - await navButtons[ 0 ].click(); -} - -describe( 'Correctly Renders Block Icons on Inserter and Inspector', () => { - const dashIconRegex = /.*?<\/span>/; - const circleString = - ''; - const svgIcon = new RegExp( - `${ circleString }` - ); - - const validateSvgIcon = ( iconHtml ) => { - expect( iconHtml ).toMatch( svgIcon ); - }; - - const validateDashIcon = ( iconHtml ) => { - expect( iconHtml ).toMatch( dashIconRegex ); - }; - - beforeAll( async () => { - await activatePlugin( 'gutenberg-test-block-icons' ); - } ); - - beforeEach( async () => { - await createNewPost(); - } ); - - afterAll( async () => { - await deactivatePlugin( 'gutenberg-test-block-icons' ); - } ); - - function testIconsOfBlock( blockName, blockTitle, validateIcon ) { - it( 'Renders correctly the icon in the inserter', async () => { - await searchForBlock( blockTitle ); - validateIcon( await getFirstInserterIcon() ); - } ); - - it( 'Can insert the block', async () => { - await insertBlock( blockTitle ); - expect( - await getInnerHTML( - `[data-type="${ blockName }"] [data-type="core/paragraph"]` - ) - ).toEqual( blockTitle ); - } ); - - it( 'Renders correctly the icon on the inspector', async () => { - await insertBlock( blockTitle ); - await openDocumentSettingsSidebar(); - await selectFirstBlock(); - validateIcon( await getInnerHTML( INSPECTOR_ICON_SELECTOR ) ); - } ); - } - - describe( 'Block with svg icon', () => { - const blockName = 'test/test-single-svg-icon'; - const blockTitle = 'TestSimpleSvgIcon'; - testIconsOfBlock( blockName, blockTitle, validateSvgIcon ); - } ); - - describe( 'Block with dash icon', () => { - const blockName = 'test/test-dash-icon'; - const blockTitle = 'TestDashIcon'; - testIconsOfBlock( blockName, blockTitle, validateDashIcon ); - } ); - - describe( 'Block with function icon', () => { - const blockName = 'test/test-function-icon'; - const blockTitle = 'TestFunctionIcon'; - testIconsOfBlock( blockName, blockTitle, validateSvgIcon ); - } ); - - describe( 'Block with dash icon and background and foreground colors', () => { - const blockTitle = 'TestDashIconColors'; - it( 'Renders the icon in the inserter with the correct colors', async () => { - await searchForBlock( blockTitle ); - validateDashIcon( await getFirstInserterIcon() ); - expect( - await getBackgroundColor( INSERTER_ICON_WRAPPER_SELECTOR ) - ).toEqual( 'rgb(1, 0, 0)' ); - expect( await getColor( INSERTER_ICON_WRAPPER_SELECTOR ) ).toEqual( - 'rgb(254, 0, 0)' - ); - } ); - - it( 'Renders the icon in the inspector with the correct colors', async () => { - await insertBlock( blockTitle ); - await openDocumentSettingsSidebar(); - await selectFirstBlock(); - validateDashIcon( await getInnerHTML( INSPECTOR_ICON_SELECTOR ) ); - expect( - await getBackgroundColor( INSPECTOR_ICON_SELECTOR ) - ).toEqual( 'rgb(1, 0, 0)' ); - expect( await getColor( INSPECTOR_ICON_SELECTOR ) ).toEqual( - 'rgb(254, 0, 0)' - ); - } ); - } ); - - describe( 'Block with svg icon and background color', () => { - const blockTitle = 'TestSvgIconBackground'; - it( 'Renders the icon in the inserter with the correct background color and an automatically compute readable foreground color', async () => { - await searchForBlock( blockTitle ); - validateSvgIcon( await getFirstInserterIcon() ); - expect( - await getBackgroundColor( INSERTER_ICON_WRAPPER_SELECTOR ) - ).toEqual( 'rgb(1, 0, 0)' ); - expect( await getColor( INSERTER_ICON_WRAPPER_SELECTOR ) ).toEqual( - 'rgb(248, 249, 249)' - ); - } ); - - it( 'Renders correctly the icon on the inspector', async () => { - await insertBlock( blockTitle ); - await openDocumentSettingsSidebar(); - await selectFirstBlock(); - validateSvgIcon( await getInnerHTML( INSPECTOR_ICON_SELECTOR ) ); - expect( - await getBackgroundColor( INSPECTOR_ICON_SELECTOR ) - ).toEqual( 'rgb(1, 0, 0)' ); - expect( await getColor( INSPECTOR_ICON_SELECTOR ) ).toEqual( - 'rgb(248, 249, 249)' - ); - } ); - } ); -} ); diff --git a/packages/e2e-tests/specs/editor/various/is-typing.test.js b/packages/e2e-tests/specs/editor/various/is-typing.test.js deleted file mode 100644 index c6208470ffb8e5..00000000000000 --- a/packages/e2e-tests/specs/editor/various/is-typing.test.js +++ /dev/null @@ -1,101 +0,0 @@ -/** - * WordPress dependencies - */ -import { - clickBlockAppender, - createNewPost, - showBlockToolbar, -} from '@wordpress/e2e-test-utils'; - -describe( 'isTyping', () => { - beforeEach( async () => { - await createNewPost(); - } ); - - it( 'should hide the toolbar when typing', async () => { - const blockToolbarSelector = '.block-editor-block-toolbar'; - - await clickBlockAppender(); - - // Type in a paragraph. - await page.keyboard.type( 'Type' ); - - // Toolbar is hidden - let blockToolbar = await page.$( blockToolbarSelector ); - expect( blockToolbar ).toBe( null ); - - // Moving the mouse shows the toolbar. - await showBlockToolbar(); - - // Toolbar is visible. - blockToolbar = await page.$( blockToolbarSelector ); - expect( blockToolbar ).not.toBe( null ); - - // Typing again hides the toolbar - await page.keyboard.type( ' and continue' ); - - // Toolbar is hidden again - blockToolbar = await page.$( blockToolbarSelector ); - expect( blockToolbar ).toBe( null ); - } ); - - it( 'should not close the dropdown when typing in it', async () => { - // Adds a Dropdown with an input to all blocks. - await page.evaluate( () => { - const { Dropdown, ToolbarButton, Fill } = wp.components; - const { createElement: el, Fragment } = wp.element; - function AddDropdown( BlockListBlock ) { - return ( props ) => { - return el( - Fragment, - {}, - el( - Fill, - { name: 'BlockControls' }, - el( Dropdown, { - renderToggle: ( { onToggle } ) => - el( - ToolbarButton, - { - onClick: onToggle, - className: 'dropdown-open', - }, - 'Open Dropdown' - ), - renderContent: () => - el( 'input', { - className: 'dropdown-input', - } ), - } ) - ), - el( BlockListBlock, props ) - ); - }; - } - - wp.hooks.addFilter( - 'editor.BlockListBlock', - 'e2e-test/add-dropdown', - AddDropdown - ); - } ); - - await clickBlockAppender(); - - // Type in a paragraph. - await page.keyboard.type( 'Type' ); - - // Show Toolbar. - await showBlockToolbar(); - - // Open the dropdown. - await page.click( '.dropdown-open' ); - - // Type inside the dropdown's input - await page.type( '.dropdown-input', 'Random' ); - - // The input should still be visible. - const input = await page.$( '.dropdown-input' ); - expect( input ).not.toBe( null ); - } ); -} ); diff --git a/packages/edit-post/CHANGELOG.md b/packages/edit-post/CHANGELOG.md index e8e0fa1632a7da..70c562e812f1ea 100644 --- a/packages/edit-post/CHANGELOG.md +++ b/packages/edit-post/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 7.24.0 (2023-11-29) + ## 7.23.0 (2023-11-16) ## 7.22.0 (2023-11-02) diff --git a/packages/edit-post/package.json b/packages/edit-post/package.json index 31c5c209fa96c2..0bc4376cedec94 100644 --- a/packages/edit-post/package.json +++ b/packages/edit-post/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/edit-post", - "version": "7.23.0", + "version": "7.24.0", "description": "Edit Post module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/edit-post/src/components/text-editor/style.scss b/packages/edit-post/src/components/text-editor/style.scss index 925e88df27180b..c02e983057e6ef 100644 --- a/packages/edit-post/src/components/text-editor/style.scss +++ b/packages/edit-post/src/components/text-editor/style.scss @@ -5,7 +5,8 @@ flex-grow: 1; // Post title. - .editor-post-title { + .editor-post-title:not(.is-raw-text), + .editor-post-title.is-raw-text textarea { max-width: none; line-height: $default-line-height; @@ -14,6 +15,7 @@ font-weight: normal; border: $border-width solid $gray-600; + border-radius: 0; // Same padding as body. padding: $grid-unit-20; diff --git a/packages/edit-site/CHANGELOG.md b/packages/edit-site/CHANGELOG.md index b8814356b2b6b4..de75adec15e5a9 100644 --- a/packages/edit-site/CHANGELOG.md +++ b/packages/edit-site/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 5.24.0 (2023-11-29) + ## 5.23.0 (2023-11-16) ## 5.22.0 (2023-11-02) diff --git a/packages/edit-site/package.json b/packages/edit-site/package.json index 072dc0b1c027a8..2ff4f10c084a88 100644 --- a/packages/edit-site/package.json +++ b/packages/edit-site/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/edit-site", - "version": "5.23.0", + "version": "5.24.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 c90ffde135a01d..ca673e3867bdaf 100644 --- a/packages/edit-site/src/components/actions/index.js +++ b/packages/edit-site/src/components/actions/index.js @@ -10,6 +10,13 @@ import { __, sprintf } from '@wordpress/i18n'; import { store as noticesStore } from '@wordpress/notices'; import { useMemo } from '@wordpress/element'; import { privateApis as routerPrivateApis } from '@wordpress/router'; +import { + Button, + __experimentalText as Text, + __experimentalHStack as HStack, + __experimentalVStack as VStack, +} from '@wordpress/components'; + /** * Internal dependencies */ @@ -17,55 +24,76 @@ import { unlock } from '../../lock-unlock'; const { useHistory } = unlock( routerPrivateApis ); -export function useTrashPostAction() { - const { createSuccessNotice, createErrorNotice } = - useDispatch( noticesStore ); - const { deleteEntityRecord } = useDispatch( coreStore ); - - return useMemo( - () => ( { - id: 'move-to-trash', - label: __( 'Move to Trash' ), - isPrimary: true, - icon: trash, - isEligible( { status } ) { - return status !== 'trash'; - }, - async callback( post ) { - try { - await deleteEntityRecord( - 'postType', - post.type, - post.id, - {}, - { throwOnError: true } - ); - createSuccessNotice( - sprintf( - /* translators: The page's title. */ - __( '"%s" moved to the Trash.' ), - decodeEntities( post.title.rendered ) - ), - { - type: 'snackbar', - id: 'edit-site-page-trashed', - } - ); - } catch ( error ) { - const errorMessage = - error.message && error.code !== 'unknown_error' - ? error.message - : __( - 'An error occurred while moving the page to the trash.' - ); +export const trashPostAction = { + id: 'move-to-trash', + label: __( 'Move to Trash' ), + isPrimary: true, + icon: trash, + isEligible( { status } ) { + return status !== 'trash'; + }, + hideModalHeader: true, + RenderModal: ( { item: post, closeModal } ) => { + const { createSuccessNotice, createErrorNotice } = + useDispatch( noticesStore ); + const { deleteEntityRecord } = useDispatch( coreStore ); + return ( + + + { sprintf( + // translators: %s: The page's title. + __( 'Are you sure you want to delete "%s"?' ), + decodeEntities( post.title.rendered ) + ) } + + + + + + + ); + }, +}; export function usePermanentlyDeletePostAction() { const { createSuccessNotice, createErrorNotice } = diff --git a/packages/edit-site/src/components/dataviews/README.md b/packages/edit-site/src/components/dataviews/README.md index 9c6725a39ead26..9f0c7a61087c91 100644 --- a/packages/edit-site/src/components/dataviews/README.md +++ b/packages/edit-site/src/components/dataviews/README.md @@ -64,7 +64,9 @@ Example: - `operator`: which type of filter it is. Only `in` available at the moment. - `value`: the actual value selected by the user. - `hiddenFields`: the `id` of the fields that are hidden in the UI. -- `layout`: ... +- `layout`: config that is specific to a particular layout type. + - `mediaField`: used by the `grid` layout. The `id` of the field to be used for rendering each card's media. + - `primaryField`: used by the `grid` layout. The `id` of the field to be used for rendering each card's title. ### View <=> data @@ -184,6 +186,6 @@ Array of operations that can be performed upon each record. Each action is an ob - `icon`: icon to show for primary actions. It's required for a primary action, otherwise the action would be considered secondary. - `isEligible`: function, optional. Whether the action can be performed for a given record. If not present, the action is considered to be eligible for all items. It takes the given record as input. - `isDestructive`: boolean, optional. Whether the action can delete data, in which case the UI would communicate it via red color. -- `callback`: function, required. Callback function that takes the record as input and performs the required action. -- `RenderModal`: ReactElement, optional. If an action requires to render contents in a modal, can provide a component which takes as input the record and a `closeModal` function. If this prop is provided, the `callback` property would be ignored. +- `callback`: function, required unless `RenderModal` is provided. Callback function that takes the record as input and performs the required action. +- `RenderModal`: ReactElement, optional. If an action requires that some UI be rendered in a modal, it can provide a component which takes as props the record as `item` and a `closeModal` function. When this prop is provided, the `callback` property is ignored. - `hideModalHeader`: boolean, optional. This property is used in combination with `RenderModal` and controls the visibility of the modal's header. If the action renders a modal and doesn't hide the header, the action's label is going to be used in the modal's header. diff --git a/packages/edit-site/src/components/dataviews/dataviews.js b/packages/edit-site/src/components/dataviews/dataviews.js index 78d0ea83abb8ee..56a9cfd7c6ae38 100644 --- a/packages/edit-site/src/components/dataviews/dataviews.js +++ b/packages/edit-site/src/components/dataviews/dataviews.js @@ -71,14 +71,12 @@ export default function DataViews( { onChangeView={ onChangeView } /> - - - + + { !! primaryActions.length && primaryActions.map( ( action ) => { if ( !! action.RenderModal ) { diff --git a/packages/edit-site/src/components/dataviews/style.scss b/packages/edit-site/src/components/dataviews/style.scss index 2d403caa6d4e06..68a00b81bb63f6 100644 --- a/packages/edit-site/src/components/dataviews/style.scss +++ b/packages/edit-site/src/components/dataviews/style.scss @@ -34,9 +34,9 @@ } th { text-align: left; + color: var(--wp-components-color-foreground, $gray-900); font-weight: normal; - padding: 0 $grid-unit-20 $grid-unit-20; - color: $gray-700; + font-size: $default-font-size; } td, th { @@ -65,13 +65,64 @@ } } -.dataviews-view-grid__media { - width: 100%; - min-height: 200px; +.dataviews-grid-view { + margin-bottom: $grid-unit-30; + grid-template-columns: repeat(2, minmax(0, 1fr)) !important; + + @include break-xlarge() { + grid-template-columns: repeat(3, minmax(0, 1fr)) !important; // Todo: eliminate !important dependency + } + + @include break-huge() { + grid-template-columns: repeat(4, minmax(0, 1fr)) !important; // Todo: eliminate !important dependency + } + + .dataviews-view-grid__card { + h3 { // Todo: A better way to target this + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + } + + .dataviews-view-grid__media { + width: 100%; + min-height: 200px; + aspect-ratio: 1/1; + box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1); + border-radius: $radius-block-ui * 2; + overflow: hidden; - > * { - max-width: 100%; - object-fit: cover; + > * { + object-fit: cover; + width: 100%; + height: 100%; + } + } + + .dataviews-view-grid__title { + min-height: $grid-unit-30; + + a { + color: $gray-900; + text-decoration: none; + font-weight: 500; + } + } + + .dataviews-view-grid__fields { + position: relative; + font-size: 12px; + line-height: 16px; + + .dataviews-view-grid__field { + .dataviews-view-grid__field-header { + color: $gray-700; + } + .dataviews-view-grid__field-value { + color: $gray-900; + } + } } } diff --git a/packages/edit-site/src/components/dataviews/view-actions.js b/packages/edit-site/src/components/dataviews/view-actions.js index bcc63c04117d76..d3c6558e824f86 100644 --- a/packages/edit-site/src/components/dataviews/view-actions.js +++ b/packages/edit-site/src/components/dataviews/view-actions.js @@ -139,7 +139,8 @@ function PageSizeMenu( { view, onChangeView } ) { function FieldsVisibilityMenu( { view, onChangeView, fields } ) { const hidableFields = fields.filter( - ( field ) => field.enableHiding !== false + ( field ) => + field.enableHiding !== false && field.id !== view.layout.mediaField ); if ( ! hidableFields?.length ) { return null; diff --git a/packages/edit-site/src/components/dataviews/view-grid.js b/packages/edit-site/src/components/dataviews/view-grid.js index fd74d4f401a966..597f3b13bd3091 100644 --- a/packages/edit-site/src/components/dataviews/view-grid.js +++ b/packages/edit-site/src/components/dataviews/view-grid.js @@ -5,8 +5,6 @@ import { __experimentalGrid as Grid, __experimentalHStack as HStack, __experimentalVStack as VStack, - FlexBlock, - Placeholder, } from '@wordpress/components'; import { useAsyncList } from '@wordpress/compose'; @@ -19,10 +17,15 @@ export function ViewGrid( { data, fields, view, actions, getItemId } ) { const mediaField = fields.find( ( field ) => field.id === view.layout.mediaField ); + const primaryField = fields.find( + ( field ) => field.id === view.layout.primaryField + ); const visibleFields = fields.filter( ( field ) => ! view.hiddenFields.includes( field.id ) && - field.id !== view.layout.mediaField + ! [ view.layout.mediaField, view.layout.primaryField ].includes( + field.id + ) ); const shownData = useAsyncList( data, { step: 3 } ); return ( @@ -32,42 +35,56 @@ export function ViewGrid( { data, fields, view, actions, getItemId } ) { alignment="top" className="dataviews-grid-view" > - { shownData.map( ( item, index ) => { - return ( - -
- { mediaField?.render( { item, view } ) || ( - - ) } -
- - - - - { visibleFields.map( ( field ) => ( -
- { field.render( { item, view } ) } -
- ) ) } + { shownData.map( ( item, index ) => ( + +
+ { mediaField?.render( { item, view } ) } +
+ + { primaryField?.render( { item, view } ) } + + + + { visibleFields.map( ( field ) => { + const renderedValue = field.render( { + item, + view, + } ); + if ( ! renderedValue ) { + return null; + } + return ( + +
+ { field.header } +
+
+ { field.render( { item, view } ) } +
-
- - - -
+ ); + } ) }
- ); - } ) } + + ) ) } ); } diff --git a/packages/edit-site/src/components/list/added-by.js b/packages/edit-site/src/components/list/added-by.js index e9c8df0fa7f263..a7ed2c4099547f 100644 --- a/packages/edit-site/src/components/list/added-by.js +++ b/packages/edit-site/src/components/list/added-by.js @@ -22,20 +22,10 @@ import { _x } from '@wordpress/i18n'; /** * Internal dependencies */ -import { - TEMPLATE_POST_TYPE, - TEMPLATE_PART_POST_TYPE, - TEMPLATE_ORIGINS, -} from '../../utils/constants'; +import { TEMPLATE_POST_TYPE, TEMPLATE_ORIGINS } from '../../utils/constants'; /** @typedef {'wp_template'|'wp_template_part'} TemplateType */ -/** @type {TemplateType} */ -const TEMPLATE_POST_TYPE_NAMES = [ - TEMPLATE_POST_TYPE, - TEMPLATE_PART_POST_TYPE, -]; - /** * @typedef {'theme'|'plugin'|'site'|'user'} AddedByType * @@ -55,8 +45,6 @@ export function useAddedBy( postType, postId ) { return useSelect( ( select ) => { const { - getTheme, - getPlugin, getEntityRecord, getMedia, getUser, @@ -67,82 +55,54 @@ export function useAddedBy( postType, postId ) { postType, postId ); + const originalSource = template?.original_source; + const authorText = template?.author_text; - if ( TEMPLATE_POST_TYPE_NAMES.includes( template.type ) ) { - // Added by theme. - // Template originally provided by a theme, but customized by a user. - // Templates originally didn't have the 'origin' field so identify - // older customized templates by checking for no origin and a 'theme' - // or 'custom' source. - if ( - template.has_theme_file && - ( template.origin === TEMPLATE_ORIGINS.theme || - ( ! template.origin && - [ - TEMPLATE_ORIGINS.theme, - TEMPLATE_ORIGINS.custom, - ].includes( template.source ) ) ) - ) { + switch ( originalSource ) { + case 'theme': { return { - type: 'theme', + type: originalSource, icon: themeIcon, - text: - getTheme( template.theme )?.name?.rendered || - template.theme, + text: authorText, isCustomized: template.source === TEMPLATE_ORIGINS.custom, }; } - - // Added by plugin. - if ( - template.has_theme_file && - template.origin === TEMPLATE_ORIGINS.plugin - ) { + case 'plugin': { return { - type: TEMPLATE_ORIGINS.plugin, + type: originalSource, icon: pluginIcon, - text: - getPlugin( template.theme )?.name || template.theme, + text: authorText, isCustomized: template.source === TEMPLATE_ORIGINS.custom, }; } - - // Added by site. - // Template was created from scratch, but has no author. Author support - // was only added to templates in WordPress 5.9. Fallback to showing the - // site logo and title. - if ( - ! template.has_theme_file && - template.source === TEMPLATE_ORIGINS.custom && - ! template.author - ) { + case 'site': { const siteData = getEntityRecord( 'root', '__unstableBase' ); return { - type: 'site', + type: originalSource, icon: globeIcon, imageUrl: siteData?.site_logo ? getMedia( siteData.site_logo )?.source_url : undefined, - text: siteData?.name, + text: authorText, + isCustomized: false, + }; + } + default: { + const user = getUser( template.author ); + return { + type: 'user', + icon: authorIcon, + imageUrl: user?.avatar_urls?.[ 48 ], + text: authorText, isCustomized: false, }; } } - - // Added by user. - const user = getUser( template.author ); - return { - type: 'user', - icon: authorIcon, - imageUrl: user?.avatar_urls?.[ 48 ], - text: user?.nickname, - isCustomized: false, - }; }, [ postType, postId ] ); diff --git a/packages/edit-site/src/components/page-pages/index.js b/packages/edit-site/src/components/page-pages/index.js index 6f0b8b942ebe36..2d3a4c659f504e 100644 --- a/packages/edit-site/src/components/page-pages/index.js +++ b/packages/edit-site/src/components/page-pages/index.js @@ -21,7 +21,7 @@ import Link from '../routes/link'; import { DataViews, viewTypeSupportsMap } from '../dataviews'; import { default as DEFAULT_VIEWS } from '../sidebar-dataviews/default-views'; import { - useTrashPostAction, + trashPostAction, usePermanentlyDeletePostAction, useRestorePostAction, postRevisionsAction, @@ -39,6 +39,7 @@ const defaultConfigPerViewType = { list: {}, grid: { mediaField: 'featured-image', + primaryField: 'title', }, }; @@ -259,7 +260,6 @@ export default function PagePages() { [ authors ] ); - const trashPostAction = useTrashPostAction(); const permanentlyDeletePostAction = usePermanentlyDeletePostAction(); const restorePostAction = useRestorePostAction(); const editPostAction = useEditPostAction(); @@ -272,12 +272,7 @@ export default function PagePages() { editPostAction, postRevisionsAction, ], - [ - trashPostAction, - permanentlyDeletePostAction, - restorePostAction, - editPostAction, - ] + [ permanentlyDeletePostAction, restorePostAction, editPostAction ] ); const onChangeView = useCallback( ( viewUpdater ) => { diff --git a/packages/edit-site/src/components/page-templates/dataviews-templates.js b/packages/edit-site/src/components/page-templates/dataviews-templates.js index 49e15dfe81c71a..7dab7192779c89 100644 --- a/packages/edit-site/src/components/page-templates/dataviews-templates.js +++ b/packages/edit-site/src/components/page-templates/dataviews-templates.js @@ -12,6 +12,7 @@ import { __experimentalText as Text, __experimentalHStack as HStack, __experimentalVStack as VStack, + VisuallyHidden, } from '@wordpress/components'; import { __, _x } from '@wordpress/i18n'; import { useState, useMemo, useCallback } from '@wordpress/element'; @@ -31,6 +32,7 @@ import Link from '../routes/link'; import { useAddedBy, AvatarImage } from '../list/added-by'; import { TEMPLATE_POST_TYPE } from '../../utils/constants'; import { DataViews } from '../dataviews'; +import { ENUMERATION_TYPE, OPERATOR_IN } from '../dataviews/constants'; import { useResetTemplateAction, deleteTemplateAction, @@ -39,7 +41,9 @@ import { import usePatternSettings from '../page-patterns/use-pattern-settings'; import { unlock } from '../../lock-unlock'; -const { ExperimentalBlockEditorProvider } = unlock( blockEditorPrivateApis ); +const { ExperimentalBlockEditorProvider, useGlobalStyle } = unlock( + blockEditorPrivateApis +); const EMPTY_ARRAY = []; @@ -47,6 +51,7 @@ const defaultConfigPerViewType = { list: {}, grid: { mediaField: 'preview', + primaryField: 'title', }, }; @@ -59,6 +64,7 @@ const DEFAULT_VIEW = { // better to keep track of the hidden ones. hiddenFields: [ 'preview' ], layout: {}, + filters: [], }; function normalizeSearchInput( input = '' ) { @@ -112,6 +118,7 @@ function AuthorField( { item } ) { function TemplatePreview( { content, viewType } ) { const settings = usePatternSettings(); + const [ backgroundColor = 'white' ] = useGlobalStyle( 'color.background' ); const blocks = useMemo( () => { return parse( content ); }, [ content ] ); @@ -129,6 +136,7 @@ function TemplatePreview( { content, viewType } ) {
@@ -143,6 +151,20 @@ export default function DataviewsTemplates() { per_page: -1, } ); + const authors = useMemo( () => { + if ( ! allTemplates ) { + return EMPTY_ARRAY; + } + const authorsSet = new Set(); + allTemplates.forEach( ( template ) => { + authorsSet.add( template.author_text ); + } ); + return Array.from( authorsSet ).map( ( author ) => ( { + value: author, + label: author, + } ) ); + }, [ allTemplates ] ); + const fields = useMemo( () => [ { @@ -173,12 +195,17 @@ export default function DataviewsTemplates() { id: 'description', getValue: ( { item } ) => item.description, render: ( { item } ) => { - return ( - item.description && ( - - { decodeEntities( item.description ) } + return item.description ? ( + decodeEntities( item.description ) + ) : ( + <> + - ) + + { __( 'No description.' ) } + + ); }, maxWidth: 200, @@ -192,9 +219,11 @@ export default function DataviewsTemplates() { return ; }, enableHiding: false, + type: ENUMERATION_TYPE, + elements: authors, }, ], - [] + [ authors ] ); const { shownTemplates, paginationInfo } = useMemo( () => { @@ -221,6 +250,21 @@ export default function DataviewsTemplates() { } ); } + // Handle filters. + if ( view.filters.length > 0 ) { + view.filters.forEach( ( filter ) => { + if ( + filter.field === 'author' && + filter.operator === OPERATOR_IN && + !! filter.value + ) { + filteredTemplates = filteredTemplates.filter( ( item ) => { + return item.author_text === filter.value; + } ); + } + } ); + } + // Handle sorting. if ( view.sort ) { const stringSortingFields = [ 'title', 'author' ]; diff --git a/packages/edit-site/src/components/page-templates/style.scss b/packages/edit-site/src/components/page-templates/style.scss index 8af0097b8e2ec4..ed4484550b48d5 100644 --- a/packages/edit-site/src/components/page-templates/style.scss +++ b/packages/edit-site/src/components/page-templates/style.scss @@ -1,9 +1,4 @@ .page-templates-preview-field { - .block-editor-block-preview__container { - border: 1px solid $gray-300; - border-radius: $radius-block-ui; - } - &.is-viewtype-list { .block-editor-block-preview__container { height: 120px; @@ -12,7 +7,7 @@ &.is-viewtype-grid { .block-editor-block-preview__container { - height: 320px; + height: auto; } } } diff --git a/packages/edit-widgets/CHANGELOG.md b/packages/edit-widgets/CHANGELOG.md index 65b83144691eb1..885b7d5aa489b8 100644 --- a/packages/edit-widgets/CHANGELOG.md +++ b/packages/edit-widgets/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 5.24.0 (2023-11-29) + ## 5.23.0 (2023-11-16) ## 5.22.0 (2023-11-02) diff --git a/packages/edit-widgets/package.json b/packages/edit-widgets/package.json index 33c7f9d75dd619..eb318c962c1c3f 100644 --- a/packages/edit-widgets/package.json +++ b/packages/edit-widgets/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/edit-widgets", - "version": "5.23.0", + "version": "5.24.0", "description": "Widgets Page module for WordPress..", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/editor/CHANGELOG.md b/packages/editor/CHANGELOG.md index 1012c6163ec292..34019a6aad78d9 100644 --- a/packages/editor/CHANGELOG.md +++ b/packages/editor/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 13.24.0 (2023-11-29) + ## 13.23.0 (2023-11-16) ## 13.22.0 (2023-11-02) diff --git a/packages/editor/package.json b/packages/editor/package.json index 5a4bf3ba7bf216..59b4c78b8235ee 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/editor", - "version": "13.23.0", + "version": "13.24.0", "description": "Enhanced block editor for WordPress posts.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/editor/src/components/post-title/index.js b/packages/editor/src/components/post-title/index.js index a61bc2f52eb842..0c3dbbf7349a17 100644 --- a/packages/editor/src/components/post-title/index.js +++ b/packages/editor/src/components/post-title/index.js @@ -168,7 +168,7 @@ function PostTitle( _, forwardedRef ) { const { ref: richTextRef } = useRichText( { value: title, onChange, - decodedPlaceholder, + placeholder: decodedPlaceholder, selectionStart: selection.start, selectionEnd: selection.end, onSelectionChange( newStart, newEnd ) { diff --git a/packages/editor/src/components/post-title/post-title-raw.js b/packages/editor/src/components/post-title/post-title-raw.js index b6a52e43731926..f59ec40e872e45 100644 --- a/packages/editor/src/components/post-title/post-title-raw.js +++ b/packages/editor/src/components/post-title/post-title-raw.js @@ -73,6 +73,7 @@ function PostTitleRaw( _, forwardedRef ) { hideLabelFromVision={ true } autoComplete="off" dir="auto" + rows={ 1 } __nextHasNoMarginBottom /> ); diff --git a/packages/editor/src/components/post-title/style.scss b/packages/editor/src/components/post-title/style.scss index bf667c39933bdf..98bdfb9a2ebf3a 100644 --- a/packages/editor/src/components/post-title/style.scss +++ b/packages/editor/src/components/post-title/style.scss @@ -1,4 +1,5 @@ -.edit-post-text-editor__body .is-raw-text textarea { - font-size: inherit; - line-height: inherit; +// Raw Text Variant +.edit-post-text-editor__body .editor-post-title.is-raw-text { + margin-bottom: $grid-unit-30; + margin-top: 2px; // space for focus outline to appear. } diff --git a/packages/editor/src/components/provider/index.js b/packages/editor/src/components/provider/index.js index 6be4d6d6fe4607..dda536aec4f733 100644 --- a/packages/editor/src/components/provider/index.js +++ b/packages/editor/src/components/provider/index.js @@ -111,7 +111,7 @@ function useBlockEditorProps( post, template, mode ) { useEntityBlockEditor( 'postType', template?.type, { id: template?.id, } ); - const blocks = useMemo( () => { + const maybeNavigationBlocks = useMemo( () => { if ( post.type === 'wp_navigation' ) { return [ createBlock( 'core/navigation', { @@ -123,7 +123,9 @@ function useBlockEditorProps( post, template, mode ) { } ), ]; } + }, [ post.type, post.id ] ); + const maybePostOnlyBlocks = useMemo( () => { if ( mode === 'post-only' ) { const postContentBlocks = extractPageContentBlockTypesFromTemplateBlocks( @@ -151,6 +153,18 @@ function useBlockEditorProps( post, template, mode ) { ), ]; } + }, [ templateBlocks, mode ] ); + + // It is important that we don't create a new instance of blocks on every change + // We should only create a new instance if the blocks them selves change, not a dependency of them. + const blocks = useMemo( () => { + if ( maybeNavigationBlocks ) { + return maybeNavigationBlocks; + } + + if ( maybePostOnlyBlocks ) { + return maybePostOnlyBlocks; + } if ( rootLevelPost === 'template' ) { return templateBlocks; @@ -158,13 +172,16 @@ function useBlockEditorProps( post, template, mode ) { return postBlocks; }, [ + maybeNavigationBlocks, + maybePostOnlyBlocks, + rootLevelPost, templateBlocks, postBlocks, - rootLevelPost, - post.type, - post.id, - mode, ] ); + + // Handle fallback to postBlocks outside of the above useMemo, to ensure + // that constructed block templates that call `createBlock` are not generated + // too frequently. This ensures that clientIds are stable. const disableRootLevelChanges = ( !! template && mode === 'template-locked' ) || post.type === 'wp_navigation' || diff --git a/packages/element/CHANGELOG.md b/packages/element/CHANGELOG.md index a3d999401c24be..574f801a1d7b7e 100644 --- a/packages/element/CHANGELOG.md +++ b/packages/element/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 5.24.0 (2023-11-29) + ## 5.23.0 (2023-11-16) ## 5.22.0 (2023-11-02) diff --git a/packages/element/package.json b/packages/element/package.json index b8753b952909b6..c40b80802d4a0a 100644 --- a/packages/element/package.json +++ b/packages/element/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/element", - "version": "5.23.0", + "version": "5.24.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 b1dd4f778334f7..aeae4f73e766fe 100644 --- a/packages/env/CHANGELOG.md +++ b/packages/env/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 8.13.0 (2023-11-29) + ## 8.12.0 (2023-11-16) ## 8.11.0 (2023-11-02) diff --git a/packages/env/package.json b/packages/env/package.json index 28373f4f391b6a..94ee81a31d59b6 100644 --- a/packages/env/package.json +++ b/packages/env/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/env", - "version": "8.12.0", + "version": "8.13.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 9f62bcec394e90..8be716a9504a8f 100644 --- a/packages/escape-html/CHANGELOG.md +++ b/packages/escape-html/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 2.47.0 (2023-11-29) + ## 2.46.0 (2023-11-16) ## 2.45.0 (2023-11-02) diff --git a/packages/escape-html/package.json b/packages/escape-html/package.json index e45800c248dfc3..7eacfe4f0e0a22 100644 --- a/packages/escape-html/package.json +++ b/packages/escape-html/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/escape-html", - "version": "2.46.0", + "version": "2.47.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 203f5246c10f46..111dcbde574e63 100644 --- a/packages/eslint-plugin/CHANGELOG.md +++ b/packages/eslint-plugin/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 17.4.0 (2023-11-29) + ## 17.3.0 (2023-11-16) ## 17.2.0 (2023-11-02) diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json index f3009f7a584f64..4ea51fab8bab98 100644 --- a/packages/eslint-plugin/package.json +++ b/packages/eslint-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/eslint-plugin", - "version": "17.3.0", + "version": "17.4.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 ca29a28fd3a4c0..19da743a909436 100644 --- a/packages/format-library/CHANGELOG.md +++ b/packages/format-library/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 4.24.0 (2023-11-29) + ## 4.23.0 (2023-11-16) ## 4.22.0 (2023-11-02) diff --git a/packages/format-library/package.json b/packages/format-library/package.json index f7b9dc90224e64..1f1ff7700a002e 100644 --- a/packages/format-library/package.json +++ b/packages/format-library/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/format-library", - "version": "4.23.0", + "version": "4.24.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 d2f43c203979c3..d8355c0fd472a3 100644 --- a/packages/hooks/CHANGELOG.md +++ b/packages/hooks/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 3.47.0 (2023-11-29) + ## 3.46.0 (2023-11-16) ## 3.45.0 (2023-11-02) diff --git a/packages/hooks/package.json b/packages/hooks/package.json index 6b9a3fccd3c7ad..e33b95790c7692 100644 --- a/packages/hooks/package.json +++ b/packages/hooks/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/hooks", - "version": "3.46.0", + "version": "3.47.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 bf89467f2154e8..a7cb330e988496 100644 --- a/packages/html-entities/CHANGELOG.md +++ b/packages/html-entities/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 3.47.0 (2023-11-29) + ## 3.46.0 (2023-11-16) ## 3.45.0 (2023-11-02) diff --git a/packages/html-entities/package.json b/packages/html-entities/package.json index 8d30d7c48173c0..1b5a775a08ec0f 100644 --- a/packages/html-entities/package.json +++ b/packages/html-entities/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/html-entities", - "version": "3.46.0", + "version": "3.47.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 dfc007b50cd076..56b7c0ba885c56 100644 --- a/packages/i18n/CHANGELOG.md +++ b/packages/i18n/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 4.47.0 (2023-11-29) + ## 4.46.0 (2023-11-16) ## 4.45.0 (2023-11-02) diff --git a/packages/i18n/package.json b/packages/i18n/package.json index 47414963190cdf..33a9e59fe2d51b 100644 --- a/packages/i18n/package.json +++ b/packages/i18n/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/i18n", - "version": "4.46.0", + "version": "4.47.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 a906ef9f569d21..827234758766d0 100644 --- a/packages/icons/CHANGELOG.md +++ b/packages/icons/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 9.38.0 (2023-11-29) + ## 9.37.0 (2023-11-16) ### New features diff --git a/packages/icons/package.json b/packages/icons/package.json index 5cef81d4bc67b6..a357de3a39f261 100644 --- a/packages/icons/package.json +++ b/packages/icons/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/icons", - "version": "9.37.0", + "version": "9.38.0", "description": "WordPress Icons package, based on dashicon.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/icons/src/library/trash.js b/packages/icons/src/library/trash.js index 95a391ca1f6097..79870537dbb633 100644 --- a/packages/icons/src/library/trash.js +++ b/packages/icons/src/library/trash.js @@ -5,7 +5,11 @@ import { SVG, Path } from '@wordpress/primitives'; const trash = ( - + ); diff --git a/packages/interactivity/CHANGELOG.md b/packages/interactivity/CHANGELOG.md index 089280cee21448..3f54c4c6046d9a 100644 --- a/packages/interactivity/CHANGELOG.md +++ b/packages/interactivity/CHANGELOG.md @@ -2,6 +2,12 @@ ## Unreleased +## 3.0.0 (2023-11-29) + +### Breaking Change + +- Implement the new `store()` API as specified in the [proposal](https://github.com/WordPress/gutenberg/discussions/53586). ([#55459](https://github.com/WordPress/gutenberg/pull/55459)) + ## 2.7.0 (2023-11-16) ## 2.6.0 (2023-11-02) diff --git a/packages/interactivity/package.json b/packages/interactivity/package.json index fd2491695be5ad..da118762e3c02a 100644 --- a/packages/interactivity/package.json +++ b/packages/interactivity/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/interactivity", - "version": "2.7.0", + "version": "3.0.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", @@ -24,6 +24,7 @@ "main": "build/index.js", "module": "build-module/index.js", "react-native": "src/index", + "types": "build-types", "dependencies": { "@preact/signals": "^1.1.3", "deepsignal": "^1.3.6", diff --git a/packages/interactivity/src/directives.js b/packages/interactivity/src/directives.js index ce3859c630231f..0793dc0cc5d5ba 100644 --- a/packages/interactivity/src/directives.js +++ b/packages/interactivity/src/directives.js @@ -17,6 +17,7 @@ import { createPortal } from './portals'; import { useSignalEffect } from './utils'; import { directive } from './hooks'; import { SlotProvider, Slot, Fill } from './slots'; +import { navigate } from './router'; const isObject = ( item ) => item && typeof item === 'object' && ! Array.isArray( item ); @@ -40,21 +41,24 @@ export default () => { directive( 'context', ( { - directives: { - context: { default: newContext }, - }, + directives: { context }, props: { children }, context: inheritedContext, } ) => { const { Provider } = inheritedContext; const inheritedValue = useContext( inheritedContext ); const currentValue = useRef( deepSignal( {} ) ); + const passedValues = context.map( ( { value } ) => value ); + currentValue.current = useMemo( () => { - const newValue = deepSignal( newContext ); + const newValue = context + .map( ( c ) => deepSignal( { [ c.namespace ]: c.value } ) ) + .reduceRight( mergeDeepSignals ); + mergeDeepSignals( newValue, inheritedValue ); mergeDeepSignals( currentValue.current, newValue, true ); return currentValue.current; - }, [ newContext, inheritedValue ] ); + }, [ inheritedValue, ...passedValues ] ); return ( { children } @@ -68,32 +72,25 @@ export default () => { return createPortal( children, document.body ); } ); - // data-wp-effect--[name] - directive( 'effect', ( { directives: { effect }, context, evaluate } ) => { - const contextValue = useContext( context ); - Object.values( effect ).forEach( ( path ) => { - useSignalEffect( () => { - return evaluate( path, { context: contextValue } ); - } ); + // data-wp-watch--[name] + directive( 'watch', ( { directives: { watch }, evaluate } ) => { + watch.forEach( ( entry ) => { + useSignalEffect( () => evaluate( entry ) ); } ); } ); // data-wp-init--[name] - directive( 'init', ( { directives: { init }, context, evaluate } ) => { - const contextValue = useContext( context ); - Object.values( init ).forEach( ( path ) => { - useEffect( () => { - return evaluate( path, { context: contextValue } ); - }, [] ); + directive( 'init', ( { directives: { init }, evaluate } ) => { + init.forEach( ( entry ) => { + useEffect( () => evaluate( entry ), [] ); } ); } ); // data-wp-on--[event] - directive( 'on', ( { directives: { on }, element, evaluate, context } ) => { - const contextValue = useContext( context ); - Object.entries( on ).forEach( ( [ name, path ] ) => { - element.props[ `on${ name }` ] = ( event ) => { - evaluate( path, { event, context: contextValue } ); + directive( 'on', ( { directives: { on }, element, evaluate } ) => { + on.forEach( ( entry ) => { + element.props[ `on${ entry.suffix }` ] = ( event ) => { + evaluate( entry, event ); }; } ); } ); @@ -101,20 +98,12 @@ export default () => { // data-wp-class--[classname] directive( 'class', - ( { - directives: { class: className }, - element, - evaluate, - context, - } ) => { - const contextValue = useContext( context ); - Object.keys( className ) - .filter( ( n ) => n !== 'default' ) - .forEach( ( name ) => { - const result = evaluate( className[ name ], { - className: name, - context: contextValue, - } ); + ( { directives: { class: className }, element, evaluate } ) => { + className + .filter( ( { suffix } ) => suffix !== 'default' ) + .forEach( ( entry ) => { + const name = entry.suffix; + const result = evaluate( entry, { className: name } ); const currentClass = element.props.class || ''; const classFinder = new RegExp( `(^|\\s)${ name }(\\s|$)`, @@ -179,111 +168,142 @@ export default () => { }; // data-wp-style--[style-key] - directive( - 'style', - ( { directives: { style }, element, evaluate, context } ) => { - const contextValue = useContext( context ); - Object.keys( style ) - .filter( ( n ) => n !== 'default' ) - .forEach( ( key ) => { - const result = evaluate( style[ key ], { - key, - context: contextValue, - } ); - element.props.style = element.props.style || {}; - if ( typeof element.props.style === 'string' ) - element.props.style = cssStringToObject( - element.props.style - ); - if ( ! result ) delete element.props.style[ key ]; - else element.props.style[ key ] = result; + directive( 'style', ( { directives: { style }, element, evaluate } ) => { + style + .filter( ( { suffix } ) => suffix !== 'default' ) + .forEach( ( entry ) => { + const key = entry.suffix; + const result = evaluate( entry, { key } ); + element.props.style = element.props.style || {}; + if ( typeof element.props.style === 'string' ) + element.props.style = cssStringToObject( + element.props.style + ); + if ( ! result ) delete element.props.style[ key ]; + else element.props.style[ key ] = result; - useEffect( () => { - // This seems necessary because Preact doesn't change the styles on - // the hydration, so we have to do it manually. It doesn't need deps - // because it only needs to do it the first time. - if ( ! result ) { - element.ref.current.style.removeProperty( key ); - } else { - element.ref.current.style[ key ] = result; - } - }, [] ); - } ); - } - ); + useEffect( () => { + // This seems necessary because Preact doesn't change the styles on + // the hydration, so we have to do it manually. It doesn't need deps + // because it only needs to do it the first time. + if ( ! result ) { + element.ref.current.style.removeProperty( key ); + } else { + element.ref.current.style[ key ] = result; + } + }, [] ); + } ); + } ); // data-wp-bind--[attribute] + directive( 'bind', ( { directives: { bind }, element, evaluate } ) => { + bind.filter( ( { suffix } ) => suffix !== 'default' ).forEach( + ( entry ) => { + const attribute = entry.suffix; + const result = evaluate( entry ); + element.props[ attribute ] = result; + // Preact doesn't handle the `role` attribute properly, as it doesn't remove it when `null`. + // We need this workaround until the following issue is solved: + // https://github.com/preactjs/preact/issues/4136 + useLayoutEffect( () => { + if ( + attribute === 'role' && + ( result === null || result === undefined ) + ) { + element.ref.current.removeAttribute( attribute ); + } + }, [ attribute, result ] ); + + // This seems necessary because Preact doesn't change the attributes + // on the hydration, so we have to do it manually. It doesn't need + // deps because it only needs to do it the first time. + useEffect( () => { + const el = element.ref.current; + + // We set the value directly to the corresponding + // HTMLElement instance property excluding the following + // special cases. + // We follow Preact's logic: https://github.com/preactjs/preact/blob/ea49f7a0f9d1ff2c98c0bdd66aa0cbc583055246/src/diff/props.js#L110-L129 + if ( + attribute !== 'width' && + attribute !== 'height' && + attribute !== 'href' && + attribute !== 'list' && + attribute !== 'form' && + // Default value in browsers is `-1` and an empty string is + // cast to `0` instead + attribute !== 'tabIndex' && + attribute !== 'download' && + attribute !== 'rowSpan' && + attribute !== 'colSpan' && + attribute !== 'role' && + attribute in el + ) { + try { + el[ attribute ] = + result === null || result === undefined + ? '' + : result; + return; + } catch ( err ) {} + } + // aria- and data- attributes have no boolean representation. + // A `false` value is different from the attribute not being + // present, so we can't remove it. + // We follow Preact's logic: https://github.com/preactjs/preact/blob/ea49f7a0f9d1ff2c98c0bdd66aa0cbc583055246/src/diff/props.js#L131C24-L136 + if ( + result !== null && + result !== undefined && + ( result !== false || attribute[ 4 ] === '-' ) + ) { + el.setAttribute( attribute, result ); + } else { + el.removeAttribute( attribute ); + } + }, [] ); + } + ); + } ); + + // data-wp-navigation-link directive( - 'bind', - ( { directives: { bind }, element, context, evaluate } ) => { - const contextValue = useContext( context ); - Object.entries( bind ) - .filter( ( n ) => n !== 'default' ) - .forEach( ( [ attribute, path ] ) => { - const result = evaluate( path, { - context: contextValue, - } ); - element.props[ attribute ] = result; - // Preact doesn't handle the `role` attribute properly, as it doesn't remove it when `null`. - // We need this workaround until the following issue is solved: - // https://github.com/preactjs/preact/issues/4136 - useLayoutEffect( () => { - if ( - attribute === 'role' && - ( result === null || result === undefined ) - ) { - element.ref.current.removeAttribute( attribute ); - } - }, [ attribute, result ] ); + 'navigation-link', + ( { + directives: { 'navigation-link': navigationLink }, + props: { href }, + element, + } ) => { + const { value: link } = navigationLink.find( + ( { suffix } ) => suffix === 'default' + ); - // This seems necessary because Preact doesn't change the attributes - // on the hydration, so we have to do it manually. It doesn't need - // deps because it only needs to do it the first time. - useEffect( () => { - const el = element.ref.current; - - // We set the value directly to the corresponding - // HTMLElement instance property excluding the following - // special cases. - // We follow Preact's logic: https://github.com/preactjs/preact/blob/ea49f7a0f9d1ff2c98c0bdd66aa0cbc583055246/src/diff/props.js#L110-L129 - if ( - attribute !== 'width' && - attribute !== 'height' && - attribute !== 'href' && - attribute !== 'list' && - attribute !== 'form' && - // Default value in browsers is `-1` and an empty string is - // cast to `0` instead - attribute !== 'tabIndex' && - attribute !== 'download' && - attribute !== 'rowSpan' && - attribute !== 'colSpan' && - attribute !== 'role' && - attribute in el - ) { - try { - el[ attribute ] = - result === null || result === undefined - ? '' - : result; - return; - } catch ( err ) {} - } - // aria- and data- attributes have no boolean representation. - // A `false` value is different from the attribute not being - // present, so we can't remove it. - // We follow Preact's logic: https://github.com/preactjs/preact/blob/ea49f7a0f9d1ff2c98c0bdd66aa0cbc583055246/src/diff/props.js#L131C24-L136 - if ( - result !== null && - result !== undefined && - ( result !== false || attribute[ 4 ] === '-' ) - ) { - el.setAttribute( attribute, result ); - } else { - el.removeAttribute( attribute ); - } - }, [] ); - } ); + useEffect( () => { + // Prefetch the page if it is in the directive options. + if ( link?.prefetch ) { + // prefetch( href ); + } + } ); + + // Don't do anything if it's falsy. + if ( link !== false ) { + element.props.onclick = async ( event ) => { + event.preventDefault(); + + // Fetch the page (or return it from cache). + await navigate( href ); + + // Update the scroll, depending on the option. True by default. + if ( link?.scroll === 'smooth' ) { + window.scrollTo( { + top: 0, + left: 0, + behavior: 'smooth', + } ); + } else if ( link?.scroll !== false ) { + window.scrollTo( 0, 0 ); + } + }; + } } ); @@ -308,35 +328,20 @@ export default () => { ); // data-wp-text - directive( - 'text', - ( { - directives: { - text: { default: text }, - }, - element, - evaluate, - context, - } ) => { - const contextValue = useContext( context ); - element.props.children = evaluate( text, { - context: contextValue, - } ); - } - ); + directive( 'text', ( { directives: { text }, element, evaluate } ) => { + const entry = text.find( ( { suffix } ) => suffix === 'default' ); + element.props.children = evaluate( entry ); + } ); // data-wp-slot directive( 'slot', - ( { - directives: { - slot: { default: slot }, - }, - props: { children }, - element, - } ) => { - const name = typeof slot === 'string' ? slot : slot.name; - const position = slot.position || 'children'; + ( { directives: { slot }, props: { children }, element } ) => { + const { value } = slot.find( + ( { suffix } ) => suffix === 'default' + ); + const name = typeof value === 'string' ? value : value.name; + const position = value.position || 'children'; if ( position === 'before' ) { return ( @@ -369,16 +374,9 @@ export default () => { // data-wp-fill directive( 'fill', - ( { - directives: { - fill: { default: fill }, - }, - props: { children }, - evaluate, - context, - } ) => { - const contextValue = useContext( context ); - const slot = evaluate( fill, { context: contextValue } ); + ( { directives: { fill }, props: { children }, evaluate } ) => { + const entry = fill.find( ( { suffix } ) => suffix === 'default' ); + const slot = evaluate( entry ); return { children }; }, { priority: 4 } diff --git a/packages/interactivity/src/hooks.js b/packages/interactivity/src/hooks.tsx similarity index 63% rename from packages/interactivity/src/hooks.js rename to packages/interactivity/src/hooks.tsx index d5b019300fed1a..f782d998498621 100644 --- a/packages/interactivity/src/hooks.js +++ b/packages/interactivity/src/hooks.tsx @@ -1,12 +1,15 @@ +// @ts-nocheck + /** * External dependencies */ import { h, options, createContext, cloneElement } from 'preact'; -import { useRef, useCallback } from 'preact/hooks'; +import { useRef, useCallback, useContext } from 'preact/hooks'; +import { deepSignal } from 'deepsignal'; /** * Internal dependencies */ -import { rawStore as store } from './store'; +import { stores } from './store'; /** @typedef {import('preact').VNode} VNode */ /** @typedef {typeof context} Context */ @@ -37,6 +40,67 @@ import { rawStore as store } from './store'; // Main context. const context = createContext( {} ); +// Wrap the element props to prevent modifications. +const immutableMap = new WeakMap(); +const immutableError = () => { + throw new Error( + 'Please use `data-wp-bind` to modify the attributes of an element.' + ); +}; +const immutableHandlers = { + get( target, key, receiver ) { + const value = Reflect.get( target, key, receiver ); + return !! value && typeof value === 'object' + ? deepImmutable( value ) + : value; + }, + set: immutableError, + deleteProperty: immutableError, +}; +const deepImmutable = < T extends Object = {} >( target: T ): T => { + if ( ! immutableMap.has( target ) ) + immutableMap.set( target, new Proxy( target, immutableHandlers ) ); + return immutableMap.get( target ); +}; + +// Store stacks for the current scope and the default namespaces and export APIs +// to interact with them. +const scopeStack: any[] = []; +const namespaceStack: string[] = []; + +export const getContext = < T extends object >( namespace?: string ): T => + getScope()?.context[ namespace || namespaceStack.slice( -1 )[ 0 ] ]; + +export const getElement = () => { + if ( ! getScope() ) { + throw Error( + 'Cannot call `getElement()` outside getters and actions used by directives.' + ); + } + const { ref, state, props } = getScope(); + return Object.freeze( { + ref: ref.current, + state, + props: deepImmutable( props ), + } ); +}; + +export const getScope = () => scopeStack.slice( -1 )[ 0 ]; + +export const setScope = ( scope ) => { + scopeStack.push( scope ); +}; +export const resetScope = () => { + scopeStack.pop(); +}; + +export const setNamespace = ( namespace: string ) => { + namespaceStack.push( namespace ); +}; +export const resetNamespace = () => { + namespaceStack.pop(); +}; + // WordPress Directives. const directiveCallbacks = {}; const directivePriorities = {}; @@ -112,29 +176,28 @@ export const directive = ( name, callback, { priority = 10 } = {} ) => { }; // Resolve the path to some property of the store object. -const resolve = ( path, ctx ) => { - let current = { ...store, context: ctx }; +const resolve = ( path, namespace ) => { + let current = { + ...stores.get( namespace ), + context: getScope().context[ namespace ], + }; path.split( '.' ).forEach( ( p ) => ( current = current[ p ] ) ); return current; }; // Generate the evaluate function. const getEvaluate = - ( { ref } = {} ) => - ( path, extraArgs = {} ) => { + ( { scope } = {} ) => + ( entry, ...args ) => { + let { value: path, namespace } = entry; // If path starts with !, remove it and save a flag. const hasNegationOperator = path[ 0 ] === '!' && !! ( path = path.slice( 1 ) ); - const value = resolve( path, extraArgs.context ); - const returnValue = - typeof value === 'function' - ? value( { - ref: ref.current, - ...store, - ...extraArgs, - } ) - : value; - return hasNegationOperator ? ! returnValue : returnValue; + setScope( scope ); + const value = resolve( path, namespace ); + const result = typeof value === 'function' ? value( ...args ) : value; + resetScope(); + return hasNegationOperator ? ! result : result; }; // Separate directives by priority. The resulting array contains objects @@ -153,25 +216,28 @@ const getPriorityLevels = ( directives ) => { .map( ( [ , arr ] ) => arr ); }; -// Priority level wrapper. +// Component that wraps each priority level of directives of an element. const Directives = ( { directives, priorityLevels: [ currentPriorityLevel, ...nextPriorityLevels ], element, - evaluate, originalProps, - elemRef, + previousScope = {}, } ) => { - // Initialize the DOM reference. - // eslint-disable-next-line react-hooks/rules-of-hooks - elemRef = elemRef || useRef( null ); - - // Create a reference to the evaluate function using the DOM reference. - // eslint-disable-next-line react-hooks/rules-of-hooks, react-hooks/exhaustive-deps - evaluate = evaluate || useCallback( getEvaluate( { ref: elemRef } ), [] ); + // Initialize the scope of this element. These scopes are different per each + // level because each level has a different context, but they share the same + // element ref, state and props. + const scope = useRef( {} ).current; + scope.evaluate = useCallback( getEvaluate( { scope } ), [] ); + scope.context = useContext( context ); + /* eslint-disable react-hooks/rules-of-hooks */ + scope.ref = previousScope.ref || useRef( null ); + scope.state = previousScope.state || useRef( deepSignal( {} ) ).current; + /* eslint-enable react-hooks/rules-of-hooks */ - // Create a fresh copy of the vnode element. - element = cloneElement( element, { ref: elemRef } ); + // Create a fresh copy of the vnode element and add the props to the scope. + element = cloneElement( element, { ref: scope.ref } ); + scope.props = element.props; // Recursively render the wrapper for the next priority level. const children = @@ -180,22 +246,31 @@ const Directives = ( { directives={ directives } priorityLevels={ nextPriorityLevels } element={ element } - evaluate={ evaluate } originalProps={ originalProps } - elemRef={ elemRef } + previousScope={ scope } /> ) : ( element ); const props = { ...originalProps, children }; - const directiveArgs = { directives, props, element, context, evaluate }; + const directiveArgs = { + directives, + props, + element, + context, + evaluate: scope.evaluate, + }; + + setScope( scope ); for ( const directiveName of currentPriorityLevel ) { const wrapper = directiveCallbacks[ directiveName ]?.( directiveArgs ); if ( wrapper !== undefined ) props.children = wrapper; } + resetScope(); + return props.children; }; @@ -205,7 +280,10 @@ options.vnode = ( vnode ) => { if ( vnode.props.__directives ) { const props = vnode.props; const directives = props.__directives; - if ( directives.key ) vnode.key = directives.key.default; + if ( directives.key ) + vnode.key = directives.key.find( + ( { suffix } ) => suffix === 'default' + ).value; delete props.__directives; const priorityLevels = getPriorityLevels( directives ); if ( priorityLevels.length > 0 ) { diff --git a/packages/interactivity/src/index.js b/packages/interactivity/src/index.js index 88e81e6f5877c0..6c7b98e8e7a79e 100644 --- a/packages/interactivity/src/index.js +++ b/packages/interactivity/src/index.js @@ -3,9 +3,9 @@ */ import registerDirectives from './directives'; import { init } from './router'; -import { rawStore, afterLoads } from './store'; + export { store } from './store'; -export { directive } from './hooks'; +export { directive, getContext, getElement } from './hooks'; export { navigate, prefetch } from './router'; export { h as createElement } from 'preact'; export { useEffect, useContext, useMemo } from 'preact/hooks'; @@ -14,5 +14,4 @@ export { deepSignal } from 'deepsignal'; document.addEventListener( 'DOMContentLoaded', async () => { registerDirectives(); await init(); - afterLoads.forEach( ( afterLoad ) => afterLoad( rawStore ) ); } ); diff --git a/packages/interactivity/src/store.js b/packages/interactivity/src/store.js deleted file mode 100644 index e0c5f8b3fae777..00000000000000 --- a/packages/interactivity/src/store.js +++ /dev/null @@ -1,102 +0,0 @@ -/** - * External dependencies - */ -import { deepSignal } from 'deepsignal'; - -const isObject = ( item ) => - item && typeof item === 'object' && ! Array.isArray( item ); - -const deepMerge = ( target, source ) => { - if ( isObject( target ) && isObject( source ) ) { - for ( const key in source ) { - if ( isObject( source[ key ] ) ) { - if ( ! target[ key ] ) Object.assign( target, { [ key ]: {} } ); - deepMerge( target[ key ], source[ key ] ); - } else { - Object.assign( target, { [ key ]: source[ key ] } ); - } - } - } -}; - -const getSerializedState = () => { - const storeTag = document.querySelector( - `script[type="application/json"]#wp-interactivity-store-data` - ); - if ( ! storeTag ) return {}; - try { - const { state } = JSON.parse( storeTag.textContent ); - if ( isObject( state ) ) return state; - throw Error( 'Parsed state is not an object' ); - } catch ( e ) { - // eslint-disable-next-line no-console - console.log( e ); - } - return {}; -}; - -export const afterLoads = new Set(); - -const rawState = getSerializedState(); -export const rawStore = { state: deepSignal( rawState ) }; - -/** - * @typedef StoreProps Properties object passed to `store`. - * @property {Object} state State to be added to the global store. All the - * properties included here become reactive. - */ - -/** - * @typedef StoreOptions Options object. - * @property {(store:any) => void} [afterLoad] Callback to be executed after the - * Interactivity API has been set up - * and the store is ready. It - * receives the store as argument. - */ - -/** - * Extends the Interactivity API global store with the passed properties. - * - * These props typically consist of `state`, which is reactive, and other - * properties like `selectors`, `actions`, `effects`, etc. which can store - * callbacks and derived state. These props can then be referenced by any - * directive to make the HTML interactive. - * - * @example - * ```js - * store({ - * state: { - * counter: { value: 0 }, - * }, - * actions: { - * counter: { - * increment: ({ state }) => { - * state.counter.value += 1; - * }, - * }, - * }, - * }); - * ``` - * - * The code from the example above allows blocks to subscribe and interact with - * the store by using directives in the HTML, e.g.: - * - * ```html - *
- * - *
- * ``` - * - * @param {StoreProps} properties Properties to be added to the global store. - * @param {StoreOptions} [options] Options passed to the `store` call. - */ -export const store = ( { state, ...block }, { afterLoad } = {} ) => { - deepMerge( rawStore, block ); - deepMerge( rawState, state ); - if ( afterLoad ) afterLoads.add( afterLoad ); -}; diff --git a/packages/interactivity/src/store.ts b/packages/interactivity/src/store.ts new file mode 100644 index 00000000000000..1e9ab7e1a8f46b --- /dev/null +++ b/packages/interactivity/src/store.ts @@ -0,0 +1,289 @@ +/** + * External dependencies + */ +import { deepSignal } from 'deepsignal'; +import { computed } from '@preact/signals'; + +/** + * Internal dependencies + */ +import { + getScope, + setScope, + resetScope, + setNamespace, + resetNamespace, +} from './hooks'; + +const isObject = ( item: unknown ): boolean => + !! item && typeof item === 'object' && ! Array.isArray( item ); + +const deepMerge = ( target: any, source: any ) => { + if ( isObject( target ) && isObject( source ) ) { + for ( const key in source ) { + const getter = Object.getOwnPropertyDescriptor( source, key )?.get; + if ( typeof getter === 'function' ) { + Object.defineProperty( target, key, { get: getter } ); + } else if ( isObject( source[ key ] ) ) { + if ( ! target[ key ] ) Object.assign( target, { [ key ]: {} } ); + deepMerge( target[ key ], source[ key ] ); + } else { + Object.assign( target, { [ key ]: source[ key ] } ); + } + } + } +}; + +const parseInitialState = () => { + const storeTag = document.querySelector( + `script[type="application/json"]#wp-interactivity-initial-state` + ); + if ( ! storeTag?.textContent ) return {}; + try { + const initialState = JSON.parse( storeTag.textContent ); + if ( isObject( initialState ) ) return initialState; + throw Error( 'Parsed state is not an object' ); + } catch ( e ) { + // eslint-disable-next-line no-console + console.log( e ); + } + return {}; +}; + +export const stores = new Map(); +const rawStores = new Map(); +const storeLocks = new Map(); + +const objToProxy = new WeakMap(); +const proxyToNs = new WeakMap(); +const scopeToGetters = new WeakMap(); + +const proxify = ( obj: any, ns: string ) => { + if ( ! objToProxy.has( obj ) ) { + const proxy = new Proxy( obj, handlers ); + objToProxy.set( obj, proxy ); + proxyToNs.set( proxy, ns ); + } + return objToProxy.get( obj ); +}; + +const handlers = { + get: ( target: any, key: string | symbol, receiver: any ) => { + const ns = proxyToNs.get( receiver ); + + // Check if the property is a getter and we are inside an scope. If that is + // the case, we clone the getter to avoid overwriting the scoped + // dependencies of the computed each time that getter runs. + const getter = Object.getOwnPropertyDescriptor( target, key )?.get; + if ( getter ) { + const scope = getScope(); + if ( scope ) { + const getters = + scopeToGetters.get( scope ) || + scopeToGetters.set( scope, new Map() ).get( scope ); + if ( ! getters.has( getter ) ) { + getters.set( + getter, + computed( () => { + setNamespace( ns ); + setScope( scope ); + try { + return getter.call( target ); + } finally { + resetScope(); + resetNamespace(); + } + } ) + ); + } + return getters.get( getter ).value; + } + } + + const result = Reflect.get( target, key, receiver ); + + // Check if the proxy is the store root and no key with that name exist. In + // that case, return an empty object for the requested key. + if ( typeof result === 'undefined' && receiver === stores.get( ns ) ) { + const obj = {}; + Reflect.set( target, key, obj, receiver ); + return proxify( obj, ns ); + } + + // Check if the property is a generator. If it is, we turn it into an + // asynchronous function where we restore the default namespace and scope + // each time it awaits/yields. + if ( result?.constructor?.name === 'GeneratorFunction' ) { + return async ( ...args: unknown[] ) => { + const scope = getScope(); + const gen: Generator< any > = result( ...args ); + + let value: any; + let it: IteratorResult< any >; + + while ( true ) { + setNamespace( ns ); + setScope( scope ); + try { + it = gen.next( value ); + } finally { + resetScope(); + resetNamespace(); + } + + try { + value = await it.value; + } catch ( e ) { + gen.throw( e ); + } + + if ( it.done ) break; + } + + return value; + }; + } + + // Check if the property is a synchronous function. If it is, set the + // default namespace. Synchronous functions always run in the proper scope, + // which is set by the Directives component. + if ( typeof result === 'function' ) { + return ( ...args: unknown[] ) => { + setNamespace( ns ); + try { + return result( ...args ); + } finally { + resetNamespace(); + } + }; + } + + // Check if the property is an object. If it is, proxyify it. + if ( isObject( result ) ) return proxify( result, ns ); + + return result; + }, +}; + +/** + * @typedef StoreProps Properties object passed to `store`. + * @property {Object} state State to be added to the global store. All the + * properties included here become reactive. + */ + +/** + * @typedef StoreOptions Options object. + */ + +/** + * Extends the Interactivity API global store with the passed properties. + * + * These props typically consist of `state`, which is reactive, and other + * properties like `selectors`, `actions`, `effects`, etc. which can store + * callbacks and derived state. These props can then be referenced by any + * directive to make the HTML interactive. + * + * @example + * ```js + * store({ + * state: { + * counter: { value: 0 }, + * }, + * actions: { + * counter: { + * increment: ({ state }) => { + * state.counter.value += 1; + * }, + * }, + * }, + * }); + * ``` + * + * The code from the example above allows blocks to subscribe and interact with + * the store by using directives in the HTML, e.g.: + * + * ```html + *
+ * + *
+ * ``` + * + * @param {StoreProps} properties Properties to be added to the global store. + * @param {StoreOptions} [options] Options passed to the `store` call. + */ + +interface StoreOptions { + lock?: boolean | string; +} + +const universalUnlock = + 'I acknowledge that using a private store means my plugin will inevitably break on the next store release.'; + +export function store< S extends object = {} >( + namespace: string, + storePart?: S, + options?: StoreOptions +): S; +export function store< T extends object >( + namespace: string, + storePart?: T, + options?: StoreOptions +): T; + +export function store( + namespace: string, + { state = {}, ...block }: any = {}, + { lock = false }: StoreOptions = {} +) { + if ( ! stores.has( namespace ) ) { + // Lock the store if the passed lock is different from the universal + // unlock. Once the lock is set (either false, true, or a given string), + // it cannot change. + if ( lock !== universalUnlock ) { + storeLocks.set( namespace, lock ); + } + const rawStore = { state: deepSignal( state ), ...block }; + const proxiedStore = new Proxy( rawStore, handlers ); + rawStores.set( namespace, rawStore ); + stores.set( namespace, proxiedStore ); + proxyToNs.set( proxiedStore, namespace ); + } else { + // Lock the store if it wasn't locked yet and the passed lock is + // different from the universal unlock. If no lock is given, the store + // will be public and won't accept any lock from now on. + if ( lock !== universalUnlock && ! storeLocks.has( namespace ) ) { + storeLocks.set( namespace, lock ); + } else { + const storeLock = storeLocks.get( namespace ); + const isLockValid = + lock === universalUnlock || + ( lock !== true && lock === storeLock ); + + if ( ! isLockValid ) { + if ( ! storeLock ) { + throw Error( 'Cannot lock a public store' ); + } else { + throw Error( + 'Cannot unlock a private store with an invalid lock code' + ); + } + } + } + + const target = rawStores.get( namespace ); + deepMerge( target, block ); + deepMerge( target.state, state ); + } + + return stores.get( namespace ); +} + +// Parse and populate the initial state. +Object.entries( parseInitialState() ).forEach( ( [ namespace, state ] ) => { + store( namespace, { state } ); +} ); diff --git a/packages/interactivity/src/vdom.js b/packages/interactivity/src/vdom.js index 1cf4a91ec1ead5..b1342ac271a8e2 100644 --- a/packages/interactivity/src/vdom.js +++ b/packages/interactivity/src/vdom.js @@ -10,6 +10,7 @@ import { directivePrefix as p } from './constants'; const ignoreAttr = `data-${ p }-ignore`; const islandAttr = `data-${ p }-interactive`; const fullPrefix = `data-${ p }-`; +let namespace = null; // Regular expression for directive parsing. const directiveParser = new RegExp( @@ -25,6 +26,12 @@ const directiveParser = new RegExp( 'i' // Case insensitive. ); +// Regular expression for reference parsing. It can contain a namespace before +// the reference, separated by `::`, like `some-namespace::state.somePath`. +// Namespaces can contain any alphanumeric characters, hyphens, underscores or +// forward slashes. References don't have any restrictions. +const nsPathRegExp = /^([\w-_\/]+)::(.+)$/; + export const hydratedIslands = new WeakSet(); // Recursive function that transforms a DOM tree into vDOM. @@ -51,8 +58,7 @@ export function toVdom( root ) { const props = {}; const children = []; - const directives = {}; - let hasDirectives = false; + const directives = []; let ignore = false; let island = false; @@ -64,17 +70,19 @@ export function toVdom( root ) { ) { if ( n === ignoreAttr ) { ignore = true; - } else if ( n === islandAttr ) { - island = true; } else { - hasDirectives = true; - let val = attributes[ i ].value; + let [ ns, value ] = nsPathRegExp + .exec( attributes[ i ].value ) + ?.slice( 1 ) ?? [ null, attributes[ i ].value ]; try { - val = JSON.parse( val ); + value = JSON.parse( value ); } catch ( e ) {} - const [ , prefix, suffix ] = directiveParser.exec( n ); - directives[ prefix ] = directives[ prefix ] || {}; - directives[ prefix ][ suffix || 'default' ] = val; + if ( n === islandAttr ) { + island = true; + namespace = value?.namespace ?? null; + } else { + directives.push( [ n, ns, value ] ); + } } } else if ( n === 'ref' ) { continue; @@ -92,7 +100,22 @@ export function toVdom( root ) { ]; if ( island ) hydratedIslands.add( node ); - if ( hasDirectives ) props.__directives = directives; + if ( directives.length ) { + props.__directives = directives.reduce( + ( obj, [ name, ns, value ] ) => { + const [ , prefix, suffix = 'default' ] = + directiveParser.exec( name ); + if ( ! obj[ prefix ] ) obj[ prefix ] = []; + obj[ prefix ].push( { + namespace: ns ?? namespace, + value, + suffix, + } ); + return obj; + }, + {} + ); + } let child = treeWalker.firstChild(); if ( child ) { diff --git a/packages/interactivity/tsconfig.json b/packages/interactivity/tsconfig.json new file mode 100644 index 00000000000000..bcb26904e1d09d --- /dev/null +++ b/packages/interactivity/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "declarationDir": "build-types", + "checkJs": false, + "strict": false + }, + "include": [ "src/**/*" ] +} diff --git a/packages/interface/CHANGELOG.md b/packages/interface/CHANGELOG.md index 9c7370291eef60..e160bb6cef4b99 100644 --- a/packages/interface/CHANGELOG.md +++ b/packages/interface/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 5.24.0 (2023-11-29) + ## 5.23.0 (2023-11-16) ## 5.22.0 (2023-11-02) diff --git a/packages/interface/package.json b/packages/interface/package.json index 429d2ad59d0fdb..f59c65af7366be 100644 --- a/packages/interface/package.json +++ b/packages/interface/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/interface", - "version": "5.23.0", + "version": "5.24.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 9f5db485bb5405..192a27bc9fabed 100644 --- a/packages/is-shallow-equal/CHANGELOG.md +++ b/packages/is-shallow-equal/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 4.47.0 (2023-11-29) + ## 4.46.0 (2023-11-16) ## 4.45.0 (2023-11-02) diff --git a/packages/is-shallow-equal/package.json b/packages/is-shallow-equal/package.json index ffc4e97590bc97..0603d2b1499312 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.46.0", + "version": "4.47.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 4032ec50a5ebfd..75fe088a70e767 100644 --- a/packages/jest-console/CHANGELOG.md +++ b/packages/jest-console/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 7.18.0 (2023-11-29) + ## 7.17.0 (2023-11-16) ## 7.16.0 (2023-11-02) diff --git a/packages/jest-console/package.json b/packages/jest-console/package.json index 1dfb50d5bd59c2..2f2f062c4f0996 100644 --- a/packages/jest-console/package.json +++ b/packages/jest-console/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/jest-console", - "version": "7.17.0", + "version": "7.18.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 30db9b59cca7c3..de33e4d54e133a 100644 --- a/packages/jest-preset-default/CHANGELOG.md +++ b/packages/jest-preset-default/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 11.18.0 (2023-11-29) + ## 11.17.0 (2023-11-16) ## 11.16.0 (2023-11-02) diff --git a/packages/jest-preset-default/package.json b/packages/jest-preset-default/package.json index 7e3b0812e3d067..061feb00ab9e4f 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.17.0", + "version": "11.18.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 6755f2d3f41365..55961c5abf1196 100644 --- a/packages/jest-puppeteer-axe/CHANGELOG.md +++ b/packages/jest-puppeteer-axe/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 6.18.0 (2023-11-29) + ## 6.17.0 (2023-11-16) ## 6.16.0 (2023-11-02) diff --git a/packages/jest-puppeteer-axe/package.json b/packages/jest-puppeteer-axe/package.json index eed831e07d801f..9b6b6d66a91f08 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.17.0", + "version": "6.18.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 f6fb39c60931c0..1fb3ec9ea2b005 100644 --- a/packages/keyboard-shortcuts/CHANGELOG.md +++ b/packages/keyboard-shortcuts/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 4.24.0 (2023-11-29) + ## 4.23.0 (2023-11-16) ## 4.22.0 (2023-11-02) diff --git a/packages/keyboard-shortcuts/package.json b/packages/keyboard-shortcuts/package.json index dfe02e60773679..ec4b1f3e108998 100644 --- a/packages/keyboard-shortcuts/package.json +++ b/packages/keyboard-shortcuts/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/keyboard-shortcuts", - "version": "4.23.0", + "version": "4.24.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 382510e52e3676..3d24d2c0cb2e0c 100644 --- a/packages/keycodes/CHANGELOG.md +++ b/packages/keycodes/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 3.47.0 (2023-11-29) + ## 3.46.0 (2023-11-16) ## 3.45.0 (2023-11-02) diff --git a/packages/keycodes/package.json b/packages/keycodes/package.json index 4ca561d68b83d1..c98ccc24cb5d84 100644 --- a/packages/keycodes/package.json +++ b/packages/keycodes/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/keycodes", - "version": "3.46.0", + "version": "3.47.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 a092b101c7fe43..352132ddaa0146 100644 --- a/packages/lazy-import/CHANGELOG.md +++ b/packages/lazy-import/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 1.34.0 (2023-11-29) + ## 1.33.0 (2023-11-16) ## 1.32.0 (2023-11-02) diff --git a/packages/lazy-import/package.json b/packages/lazy-import/package.json index 62320482ae15bf..b490a38eccf86f 100644 --- a/packages/lazy-import/package.json +++ b/packages/lazy-import/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/lazy-import", - "version": "1.33.0", + "version": "1.34.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 3c16e7ca341d0a..5946c665d11d62 100644 --- a/packages/list-reusable-blocks/CHANGELOG.md +++ b/packages/list-reusable-blocks/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 4.24.0 (2023-11-29) + ## 4.23.0 (2023-11-16) ## 4.22.0 (2023-11-02) diff --git a/packages/list-reusable-blocks/package.json b/packages/list-reusable-blocks/package.json index 0feaddf684b5fc..29a0ab2479d926 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.23.0", + "version": "4.24.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 791752726da0e9..0329f2ea74c53c 100644 --- a/packages/media-utils/CHANGELOG.md +++ b/packages/media-utils/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 4.38.0 (2023-11-29) + ## 4.37.0 (2023-11-16) ## 4.36.0 (2023-11-02) diff --git a/packages/media-utils/package.json b/packages/media-utils/package.json index 93ed96c45246a9..1abe16387376c0 100644 --- a/packages/media-utils/package.json +++ b/packages/media-utils/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/media-utils", - "version": "4.37.0", + "version": "4.38.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 0bb727268bc07b..12abde127dc9ec 100644 --- a/packages/notices/CHANGELOG.md +++ b/packages/notices/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 4.15.0 (2023-11-29) + ## 4.14.0 (2023-11-16) ## 4.13.0 (2023-11-02) diff --git a/packages/notices/package.json b/packages/notices/package.json index 9250d196365c46..b5fdfe0377dabb 100644 --- a/packages/notices/package.json +++ b/packages/notices/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/notices", - "version": "4.14.0", + "version": "4.15.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 c4c811e7396d7e..aa45ed933d69eb 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.32.0 (2023-11-29) + ## 4.31.0 (2023-11-16) ## 4.30.0 (2023-11-02) diff --git a/packages/npm-package-json-lint-config/package.json b/packages/npm-package-json-lint-config/package.json index a62009f5d88a03..09df294498cdbb 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.31.0", + "version": "4.32.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 d3fbfa4a703809..959e582b402946 100644 --- a/packages/nux/CHANGELOG.md +++ b/packages/nux/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 8.9.0 (2023-11-29) + ## 8.8.0 (2023-11-16) ## 8.7.0 (2023-11-02) diff --git a/packages/nux/package.json b/packages/nux/package.json index 12f659accc9503..e61e63703c5d6d 100644 --- a/packages/nux/package.json +++ b/packages/nux/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/nux", - "version": "8.8.0", + "version": "8.9.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 30df46641ff4cd..416d2bfd7c22c7 100644 --- a/packages/patterns/CHANGELOG.md +++ b/packages/patterns/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 1.8.0 (2023-11-29) + ## 1.7.0 (2023-11-16) ## 1.6.0 (2023-11-02) diff --git a/packages/patterns/package.json b/packages/patterns/package.json index bab11059bf92c9..3b1cead6f71a15 100644 --- a/packages/patterns/package.json +++ b/packages/patterns/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/patterns", - "version": "1.7.0", + "version": "1.8.0", "description": "Management of user pattern editing.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/plugins/CHANGELOG.md b/packages/plugins/CHANGELOG.md index 7d6ad16901b7b2..27888ee6a6cfbd 100644 --- a/packages/plugins/CHANGELOG.md +++ b/packages/plugins/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 6.15.0 (2023-11-29) + ## 6.14.0 (2023-11-16) ## 6.13.0 (2023-11-02) diff --git a/packages/plugins/package.json b/packages/plugins/package.json index 98448a216daeab..f28b5e46de9077 100644 --- a/packages/plugins/package.json +++ b/packages/plugins/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/plugins", - "version": "6.14.0", + "version": "6.15.0", "description": "Plugins module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/postcss-plugins-preset/CHANGELOG.md b/packages/postcss-plugins-preset/CHANGELOG.md index 28088d4cfa0ae2..8c1512fb1d7e84 100644 --- a/packages/postcss-plugins-preset/CHANGELOG.md +++ b/packages/postcss-plugins-preset/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 4.31.0 (2023-11-29) + ## 4.30.0 (2023-11-16) ## 4.29.0 (2023-11-02) diff --git a/packages/postcss-plugins-preset/package.json b/packages/postcss-plugins-preset/package.json index bee5573d2f8832..8f697aee2d0826 100644 --- a/packages/postcss-plugins-preset/package.json +++ b/packages/postcss-plugins-preset/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/postcss-plugins-preset", - "version": "4.30.0", + "version": "4.31.0", "description": "PostCSS sharable plugins preset for WordPress development.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/postcss-themes/CHANGELOG.md b/packages/postcss-themes/CHANGELOG.md index 1b26ac952c41e0..4948f7afbfae42 100644 --- a/packages/postcss-themes/CHANGELOG.md +++ b/packages/postcss-themes/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 5.30.0 (2023-11-29) + ## 5.29.0 (2023-11-16) ## 5.28.0 (2023-11-02) diff --git a/packages/postcss-themes/package.json b/packages/postcss-themes/package.json index 1fd640b1b3f25e..7f0d39b7a5b171 100644 --- a/packages/postcss-themes/package.json +++ b/packages/postcss-themes/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/postcss-themes", - "version": "5.29.0", + "version": "5.30.0", "description": "PostCSS plugin to generate theme colors.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/preferences-persistence/CHANGELOG.md b/packages/preferences-persistence/CHANGELOG.md index 531a57d6130204..11066a94f0dcde 100644 --- a/packages/preferences-persistence/CHANGELOG.md +++ b/packages/preferences-persistence/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 1.39.0 (2023-11-29) + ## 1.38.0 (2023-11-16) ## 1.37.0 (2023-11-02) diff --git a/packages/preferences-persistence/package.json b/packages/preferences-persistence/package.json index 8137c703d6f336..ce3375ad43d67e 100644 --- a/packages/preferences-persistence/package.json +++ b/packages/preferences-persistence/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/preferences-persistence", - "version": "1.38.0", + "version": "1.39.0", "description": "Persistence utilities for `wordpress/preferences`.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/preferences/CHANGELOG.md b/packages/preferences/CHANGELOG.md index 29c48cd51c787a..dc6c84ad6cd5b4 100644 --- a/packages/preferences/CHANGELOG.md +++ b/packages/preferences/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 3.24.0 (2023-11-29) + ## 3.23.0 (2023-11-16) ## 3.22.0 (2023-11-02) diff --git a/packages/preferences/package.json b/packages/preferences/package.json index 78a0ed29aa3547..c2c81c22f782af 100644 --- a/packages/preferences/package.json +++ b/packages/preferences/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/preferences", - "version": "3.23.0", + "version": "3.24.0", "description": "Utilities for managing WordPress preferences.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/prettier-config/CHANGELOG.md b/packages/prettier-config/CHANGELOG.md index 3fb6aa431973fb..2ef95f2fb1d02f 100644 --- a/packages/prettier-config/CHANGELOG.md +++ b/packages/prettier-config/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 3.4.0 (2023-11-29) + ## 3.3.0 (2023-11-16) ## 3.2.0 (2023-11-02) diff --git a/packages/prettier-config/package.json b/packages/prettier-config/package.json index f97e04f7fee6c5..fc7a79934737f0 100644 --- a/packages/prettier-config/package.json +++ b/packages/prettier-config/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/prettier-config", - "version": "3.3.0", + "version": "3.4.0", "description": "WordPress Prettier shared configuration.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/primitives/CHANGELOG.md b/packages/primitives/CHANGELOG.md index 0c24a126097eea..b18081f02bf16f 100644 --- a/packages/primitives/CHANGELOG.md +++ b/packages/primitives/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 3.45.0 (2023-11-29) + ## 3.44.0 (2023-11-16) ## 3.43.0 (2023-11-02) diff --git a/packages/primitives/package.json b/packages/primitives/package.json index e3e99b3d6d0288..226b6f7998c0a9 100644 --- a/packages/primitives/package.json +++ b/packages/primitives/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/primitives", - "version": "3.44.0", + "version": "3.45.0", "description": "WordPress cross-platform primitives.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/priority-queue/CHANGELOG.md b/packages/priority-queue/CHANGELOG.md index c9e8b4c7e566cf..df26c90b131aa5 100644 --- a/packages/priority-queue/CHANGELOG.md +++ b/packages/priority-queue/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 2.47.0 (2023-11-29) + ## 2.46.0 (2023-11-16) ## 2.45.0 (2023-11-02) diff --git a/packages/priority-queue/package.json b/packages/priority-queue/package.json index 3e3ecb57872b1f..cad513efb2583c 100644 --- a/packages/priority-queue/package.json +++ b/packages/priority-queue/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/priority-queue", - "version": "2.46.0", + "version": "2.47.0", "description": "Generic browser priority queue.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/private-apis/CHANGELOG.md b/packages/private-apis/CHANGELOG.md index 65380c088560dd..16cfc9f3bd5a04 100644 --- a/packages/private-apis/CHANGELOG.md +++ b/packages/private-apis/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 0.29.0 (2023-11-29) + ## 0.28.0 (2023-11-16) ## 0.27.0 (2023-11-02) diff --git a/packages/private-apis/package.json b/packages/private-apis/package.json index be77c8334dcb42..aa19e14d284ea9 100644 --- a/packages/private-apis/package.json +++ b/packages/private-apis/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/private-apis", - "version": "0.28.0", + "version": "0.29.0", "description": "Internal experimental APIs for WordPress core.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/project-management-automation/CHANGELOG.md b/packages/project-management-automation/CHANGELOG.md index 945fac3a5cb1b0..5ae304b4c04780 100644 --- a/packages/project-management-automation/CHANGELOG.md +++ b/packages/project-management-automation/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 1.46.0 (2023-11-29) + ## 1.45.0 (2023-11-16) ## 1.44.0 (2023-11-02) diff --git a/packages/project-management-automation/package.json b/packages/project-management-automation/package.json index b4c0389872dcdf..613f580fb0e5f2 100644 --- a/packages/project-management-automation/package.json +++ b/packages/project-management-automation/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/project-management-automation", - "version": "1.45.0", + "version": "1.46.0", "description": "GitHub Action that implements various automation to assist with managing the Gutenberg GitHub repository.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/react-i18n/CHANGELOG.md b/packages/react-i18n/CHANGELOG.md index 2ec98e2ed0eae4..d6acc305c22f39 100644 --- a/packages/react-i18n/CHANGELOG.md +++ b/packages/react-i18n/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 3.45.0 (2023-11-29) + ## 3.44.0 (2023-11-16) ## 3.43.0 (2023-11-02) diff --git a/packages/react-i18n/package.json b/packages/react-i18n/package.json index 662d8e288bcc07..c6a953397ef99e 100644 --- a/packages/react-i18n/package.json +++ b/packages/react-i18n/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/react-i18n", - "version": "3.44.0", + "version": "3.45.0", "description": "React bindings for @wordpress/i18n.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/react-native-editor/CHANGELOG.md b/packages/react-native-editor/CHANGELOG.md index 635937c4d8ce0b..31429cb873c7c6 100644 --- a/packages/react-native-editor/CHANGELOG.md +++ b/packages/react-native-editor/CHANGELOG.md @@ -10,6 +10,8 @@ For each user feature we should also add a importance categorization label to i --> ## Unreleased +- [*] [internal] Move InserterButton from components package to block-editor package [#56494] +- [***] Fix issue when backspacing in an empty Paragraph block [#56496] ## 1.109.0 - [*] Audio block: Improve legibility of audio file details on various background colors [#55627] diff --git a/packages/readable-js-assets-webpack-plugin/CHANGELOG.md b/packages/readable-js-assets-webpack-plugin/CHANGELOG.md index 73466451aaad3d..f266e64d9d0f80 100644 --- a/packages/readable-js-assets-webpack-plugin/CHANGELOG.md +++ b/packages/readable-js-assets-webpack-plugin/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 2.30.0 (2023-11-29) + ## 2.29.0 (2023-11-16) ## 2.28.0 (2023-11-02) diff --git a/packages/readable-js-assets-webpack-plugin/package.json b/packages/readable-js-assets-webpack-plugin/package.json index 93a525c127bc0c..d7cf02db881c37 100644 --- a/packages/readable-js-assets-webpack-plugin/package.json +++ b/packages/readable-js-assets-webpack-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/readable-js-assets-webpack-plugin", - "version": "2.29.0", + "version": "2.30.0", "description": "Generate a readable JS file for each JS asset.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/redux-routine/CHANGELOG.md b/packages/redux-routine/CHANGELOG.md index 0bd8a9e3143f61..d39cab61e6dd97 100644 --- a/packages/redux-routine/CHANGELOG.md +++ b/packages/redux-routine/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 4.47.0 (2023-11-29) + ## 4.46.0 (2023-11-16) ## 4.45.0 (2023-11-02) diff --git a/packages/redux-routine/package.json b/packages/redux-routine/package.json index 09d7b16795ae28..3dbd8557a6a609 100644 --- a/packages/redux-routine/package.json +++ b/packages/redux-routine/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/redux-routine", - "version": "4.46.0", + "version": "4.47.0", "description": "Redux middleware for generator coroutines.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/reusable-blocks/CHANGELOG.md b/packages/reusable-blocks/CHANGELOG.md index c8208c76e0b747..15a11093d280ca 100644 --- a/packages/reusable-blocks/CHANGELOG.md +++ b/packages/reusable-blocks/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 4.24.0 (2023-11-29) + ## 4.23.0 (2023-11-16) ## 4.22.0 (2023-11-02) diff --git a/packages/reusable-blocks/package.json b/packages/reusable-blocks/package.json index f21c486014fc8c..2ab00edaba81ad 100644 --- a/packages/reusable-blocks/package.json +++ b/packages/reusable-blocks/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/reusable-blocks", - "version": "4.23.0", + "version": "4.24.0", "description": "Reusable blocks utilities.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/rich-text/CHANGELOG.md b/packages/rich-text/CHANGELOG.md index 46275b44ca2a97..363ba40911fc65 100644 --- a/packages/rich-text/CHANGELOG.md +++ b/packages/rich-text/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 6.24.0 (2023-11-29) + ## 6.23.0 (2023-11-16) ## 6.22.0 (2023-11-02) diff --git a/packages/rich-text/package.json b/packages/rich-text/package.json index 4c2fe8e32adc9c..d5cfb022b662c3 100644 --- a/packages/rich-text/package.json +++ b/packages/rich-text/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/rich-text", - "version": "6.23.0", + "version": "6.24.0", "description": "Rich text value and manipulation API.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/rich-text/src/register-format-type.js b/packages/rich-text/src/register-format-type.js index b2dd048d79e6fb..c8aa45f022154c 100644 --- a/packages/rich-text/src/register-format-type.js +++ b/packages/rich-text/src/register-format-type.js @@ -71,9 +71,9 @@ export function registerFormatType( name, settings ) { return; } - if ( ! /^[_a-zA-Z]+[a-zA-Z0-9-]*$/.test( settings.className ) ) { + if ( ! /^[_a-zA-Z]+[a-zA-Z0-9_-]*$/.test( settings.className ) ) { window.console.error( - 'A class name must begin with a letter, followed by any number of hyphens, letters, or numbers.' + 'A class name must begin with a letter, followed by any number of hyphens, underscores, letters, or numbers.' ); return; } diff --git a/packages/rich-text/src/test/register-format-type.js b/packages/rich-text/src/test/register-format-type.js index 0f5c16eabf2323..a586e47945dd08 100644 --- a/packages/rich-text/src/test/register-format-type.js +++ b/packages/rich-text/src/test/register-format-type.js @@ -171,7 +171,7 @@ describe( 'registerFormatType', () => { className: 'invalid class name', } ); expect( console ).toHaveErroredWith( - 'A class name must begin with a letter, followed by any number of hyphens, letters, or numbers.' + 'A class name must begin with a letter, followed by any number of hyphens, underscores, letters, or numbers.' ); expect( format ).toBeUndefined(); } ); diff --git a/packages/router/CHANGELOG.md b/packages/router/CHANGELOG.md index e2766c9819b3ef..5ab30cce439239 100644 --- a/packages/router/CHANGELOG.md +++ b/packages/router/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 0.16.0 (2023-11-29) + ## 0.15.0 (2023-11-16) ## 0.14.0 (2023-11-02) diff --git a/packages/router/package.json b/packages/router/package.json index ae8eb364202e27..d74bf2c233627d 100644 --- a/packages/router/package.json +++ b/packages/router/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/router", - "version": "0.15.0", + "version": "0.16.0", "description": "Router API for WordPress pages.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/scripts/CHANGELOG.md b/packages/scripts/CHANGELOG.md index bf48542f5fc019..ab55ff9adf5b8e 100644 --- a/packages/scripts/CHANGELOG.md +++ b/packages/scripts/CHANGELOG.md @@ -2,6 +2,12 @@ ## Unreleased +## 26.18.0 (2023-11-29) + +### Internal + +- The bundled `jest-dev-server` dependency has been updated from `^6.0.2` to `^9.0.1` ([#33287](https://github.com/WordPress/gutenberg/pull/33287)). + ## 26.17.0 (2023-11-16) ## 26.16.0 (2023-11-02) diff --git a/packages/scripts/config/jest-environment-puppeteer/global.js b/packages/scripts/config/jest-environment-puppeteer/global.js index cbac21951c700e..1be79ffd89c668 100644 --- a/packages/scripts/config/jest-environment-puppeteer/global.js +++ b/packages/scripts/config/jest-environment-puppeteer/global.js @@ -30,8 +30,8 @@ const chalk = require( 'chalk' ); const { readConfig, getPuppeteer } = require( './config' ); let browser; - let didAlreadyRunInWatchMode = false; +let servers = []; async function setup( jestConfig = {} ) { const config = await readConfig(); @@ -51,7 +51,7 @@ async function setup( jestConfig = {} ) { if ( config.server ) { try { - await setupServer( config.server ); + servers = await setupServer( config.server ); } catch ( error ) { const { error: printError } = console; if ( error.code === ERROR_TIMEOUT ) { @@ -89,7 +89,7 @@ async function teardown( jestConfig = {} ) { } if ( ! jestConfig.watch && ! jestConfig.watchAll ) { - await teardownServer(); + await teardownServer( servers ); } } diff --git a/packages/scripts/package.json b/packages/scripts/package.json index 2a740ec41c6ab8..1002c1817b4025 100644 --- a/packages/scripts/package.json +++ b/packages/scripts/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/scripts", - "version": "26.17.0", + "version": "26.18.0", "description": "Collection of reusable scripts for WordPress development.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", @@ -63,7 +63,7 @@ "fast-glob": "^3.2.7", "filenamify": "^4.2.0", "jest": "^29.6.2", - "jest-dev-server": "^6.0.2", + "jest-dev-server": "^9.0.1", "jest-environment-jsdom": "^29.6.2", "jest-environment-node": "^29.6.2", "markdownlint-cli": "^0.31.1", diff --git a/packages/server-side-render/CHANGELOG.md b/packages/server-side-render/CHANGELOG.md index 739fa28341ef16..6f036a521989fc 100644 --- a/packages/server-side-render/CHANGELOG.md +++ b/packages/server-side-render/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 4.24.0 (2023-11-29) + ## 4.23.0 (2023-11-16) ## 4.22.0 (2023-11-02) diff --git a/packages/server-side-render/package.json b/packages/server-side-render/package.json index 3e8f81b1476fd8..26d999e4a10e4b 100644 --- a/packages/server-side-render/package.json +++ b/packages/server-side-render/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/server-side-render", - "version": "4.23.0", + "version": "4.24.0", "description": "The component used with WordPress to server-side render a preview of dynamic blocks to display in the editor.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/shortcode/CHANGELOG.md b/packages/shortcode/CHANGELOG.md index 7c2ff99196b597..cc4bd058236a62 100644 --- a/packages/shortcode/CHANGELOG.md +++ b/packages/shortcode/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 3.47.0 (2023-11-29) + ## 3.46.0 (2023-11-16) ## 3.45.0 (2023-11-02) diff --git a/packages/shortcode/package.json b/packages/shortcode/package.json index 6646c98fe46e0f..fe383905127534 100644 --- a/packages/shortcode/package.json +++ b/packages/shortcode/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/shortcode", - "version": "3.46.0", + "version": "3.47.0", "description": "Shortcode module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/style-engine/CHANGELOG.md b/packages/style-engine/CHANGELOG.md index ddb01f0b2fdaec..9f22a226e6037f 100644 --- a/packages/style-engine/CHANGELOG.md +++ b/packages/style-engine/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 1.30.0 (2023-11-29) + ## 1.29.0 (2023-11-16) ## 1.28.0 (2023-11-02) diff --git a/packages/style-engine/package.json b/packages/style-engine/package.json index cdbb5d8f272519..f6c1aa2c0c2bae 100644 --- a/packages/style-engine/package.json +++ b/packages/style-engine/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/style-engine", - "version": "1.29.0", + "version": "1.30.0", "description": "A suite of parsers and compilers for WordPress styles.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/stylelint-config/CHANGELOG.md b/packages/stylelint-config/CHANGELOG.md index c7c0ab1a036f45..7bbe9a2da0592e 100644 --- a/packages/stylelint-config/CHANGELOG.md +++ b/packages/stylelint-config/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 21.30.0 (2023-11-29) + ## 21.29.0 (2023-11-16) ## 21.28.0 (2023-11-02) diff --git a/packages/stylelint-config/package.json b/packages/stylelint-config/package.json index 6d6ce676bbce3c..ddf7f120fb5497 100644 --- a/packages/stylelint-config/package.json +++ b/packages/stylelint-config/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/stylelint-config", - "version": "21.29.0", + "version": "21.30.0", "description": "stylelint config for WordPress development.", "author": "The WordPress Contributors", "license": "MIT", diff --git a/packages/sync/CHANGELOG.md b/packages/sync/CHANGELOG.md index 4465921540906f..fa1810c68c5799 100644 --- a/packages/sync/CHANGELOG.md +++ b/packages/sync/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 0.9.0 (2023-11-29) + ## 0.8.0 (2023-11-16) ## 0.7.0 (2023-11-02) diff --git a/packages/sync/package.json b/packages/sync/package.json index 0110a281d46aeb..8bef91b2689333 100644 --- a/packages/sync/package.json +++ b/packages/sync/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/sync", - "version": "0.8.0", + "version": "0.9.0", "description": "Sync Data.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/token-list/CHANGELOG.md b/packages/token-list/CHANGELOG.md index 920705623ca7be..3c769bcec7fb77 100644 --- a/packages/token-list/CHANGELOG.md +++ b/packages/token-list/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 2.47.0 (2023-11-29) + ## 2.46.0 (2023-11-16) ## 2.45.0 (2023-11-02) diff --git a/packages/token-list/package.json b/packages/token-list/package.json index de2e1d0c4d2a58..c818471e31aa46 100644 --- a/packages/token-list/package.json +++ b/packages/token-list/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/token-list", - "version": "2.46.0", + "version": "2.47.0", "description": "Constructable, plain JavaScript DOMTokenList implementation, supporting non-browser runtimes.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/undo-manager/CHANGELOG.md b/packages/undo-manager/CHANGELOG.md index 9c7fc9d98f62c5..412281eefcee7f 100644 --- a/packages/undo-manager/CHANGELOG.md +++ b/packages/undo-manager/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 0.7.0 (2023-11-29) + ## 0.6.0 (2023-11-16) ## 0.5.0 (2023-11-02) diff --git a/packages/undo-manager/package.json b/packages/undo-manager/package.json index b1f75f83d59788..040e88dcffd059 100644 --- a/packages/undo-manager/package.json +++ b/packages/undo-manager/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/undo-manager", - "version": "0.6.0", + "version": "0.7.0", "description": "A small package to manage undo/redo.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/url/CHANGELOG.md b/packages/url/CHANGELOG.md index 66632d73e945d5..5bed123ffbfe70 100644 --- a/packages/url/CHANGELOG.md +++ b/packages/url/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 3.48.0 (2023-11-29) + ## 3.47.0 (2023-11-16) ## 3.46.0 (2023-11-02) diff --git a/packages/url/package.json b/packages/url/package.json index 6acd999314b151..d1327f4b6e8a22 100644 --- a/packages/url/package.json +++ b/packages/url/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/url", - "version": "3.47.0", + "version": "3.48.0", "description": "WordPress URL utilities.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/viewport/CHANGELOG.md b/packages/viewport/CHANGELOG.md index 02c093f466c674..7f32eaea5b2931 100644 --- a/packages/viewport/CHANGELOG.md +++ b/packages/viewport/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 5.24.0 (2023-11-29) + ## 5.23.0 (2023-11-16) ## 5.22.0 (2023-11-02) diff --git a/packages/viewport/package.json b/packages/viewport/package.json index 2357182bdfd8d7..2eca13e8f73066 100644 --- a/packages/viewport/package.json +++ b/packages/viewport/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/viewport", - "version": "5.23.0", + "version": "5.24.0", "description": "Viewport module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/warning/CHANGELOG.md b/packages/warning/CHANGELOG.md index 68504579d3188f..16dec4f7ea711b 100644 --- a/packages/warning/CHANGELOG.md +++ b/packages/warning/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 2.47.0 (2023-11-29) + ## 2.46.0 (2023-11-16) ## 2.45.0 (2023-11-02) diff --git a/packages/warning/package.json b/packages/warning/package.json index a4c0f7df23070f..42ad8f73a289d8 100644 --- a/packages/warning/package.json +++ b/packages/warning/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/warning", - "version": "2.46.0", + "version": "2.47.0", "description": "Warning utility for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/widgets/CHANGELOG.md b/packages/widgets/CHANGELOG.md index 97fd6909b10adf..b4e8a97665eaae 100644 --- a/packages/widgets/CHANGELOG.md +++ b/packages/widgets/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 3.24.0 (2023-11-29) + ## 3.23.0 (2023-11-16) ## 3.22.0 (2023-11-02) diff --git a/packages/widgets/package.json b/packages/widgets/package.json index 06dc5604703929..9118333a4e356f 100644 --- a/packages/widgets/package.json +++ b/packages/widgets/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/widgets", - "version": "3.23.0", + "version": "3.24.0", "description": "Functionality used by the widgets block editor in the Widgets screen and the Customizer.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/wordcount/CHANGELOG.md b/packages/wordcount/CHANGELOG.md index 7b2a56464d0832..75b25aaec0e97e 100644 --- a/packages/wordcount/CHANGELOG.md +++ b/packages/wordcount/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 3.47.0 (2023-11-29) + ## 3.46.0 (2023-11-16) ## 3.45.0 (2023-11-02) diff --git a/packages/wordcount/package.json b/packages/wordcount/package.json index dbb3fdd3bc1f42..f100df8c7b0a51 100644 --- a/packages/wordcount/package.json +++ b/packages/wordcount/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/wordcount", - "version": "3.46.0", + "version": "3.47.0", "description": "WordPress word count utility.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/phpunit/blocks/render-query-test.php b/phpunit/blocks/render-query-test.php index 2d81bfdb513b33..796c4ae098867c 100644 --- a/phpunit/blocks/render-query-test.php +++ b/phpunit/blocks/render-query-test.php @@ -68,32 +68,32 @@ public function test_rendering_query_with_enhanced_pagination() { $p = new WP_HTML_Tag_Processor( $output ); $p->next_tag( array( 'class_name' => 'wp-block-query' ) ); - $this->assertSame( '{"core":{"query":{"loadingText":"Loading page, please wait.","loadedText":"Page Loaded."}}}', $p->get_attribute( 'data-wp-context' ) ); + $this->assertSame( '{"loadingText":"Loading page, please wait.","loadedText":"Page Loaded."}', $p->get_attribute( 'data-wp-context' ) ); $this->assertSame( 'query-0', $p->get_attribute( 'data-wp-navigation-id' ) ); - $this->assertSame( true, $p->get_attribute( 'data-wp-interactive' ) ); + $this->assertSame( '{"namespace":"core/query"}', $p->get_attribute( 'data-wp-interactive' ) ); $p->next_tag( array( 'class_name' => 'wp-block-post' ) ); $this->assertSame( 'post-template-item-' . self::$posts[1], $p->get_attribute( 'data-wp-key' ) ); $p->next_tag( array( 'class_name' => 'wp-block-query-pagination-previous' ) ); $this->assertSame( 'query-pagination-previous', $p->get_attribute( 'data-wp-key' ) ); - $this->assertSame( 'actions.core.query.navigate', $p->get_attribute( 'data-wp-on--click' ) ); - $this->assertSame( 'actions.core.query.prefetch', $p->get_attribute( 'data-wp-on--mouseenter' ) ); - $this->assertSame( 'effects.core.query.prefetch', $p->get_attribute( 'data-wp-effect' ) ); + $this->assertSame( 'core/query::actions.navigate', $p->get_attribute( 'data-wp-on--click' ) ); + $this->assertSame( 'core/query::actions.prefetch', $p->get_attribute( 'data-wp-on--mouseenter' ) ); + $this->assertSame( 'core/query::callbacks.prefetch', $p->get_attribute( 'data-wp-watch' ) ); $p->next_tag( array( 'class_name' => 'wp-block-query-pagination-next' ) ); $this->assertSame( 'query-pagination-next', $p->get_attribute( 'data-wp-key' ) ); - $this->assertSame( 'actions.core.query.navigate', $p->get_attribute( 'data-wp-on--click' ) ); - $this->assertSame( 'actions.core.query.prefetch', $p->get_attribute( 'data-wp-on--mouseenter' ) ); - $this->assertSame( 'effects.core.query.prefetch', $p->get_attribute( 'data-wp-effect' ) ); + $this->assertSame( 'core/query::actions.navigate', $p->get_attribute( 'data-wp-on--click' ) ); + $this->assertSame( 'core/query::actions.prefetch', $p->get_attribute( 'data-wp-on--mouseenter' ) ); + $this->assertSame( 'core/query::callbacks.prefetch', $p->get_attribute( 'data-wp-watch' ) ); $p->next_tag( array( 'class_name' => 'screen-reader-text' ) ); $this->assertSame( 'polite', $p->get_attribute( 'aria-live' ) ); - $this->assertSame( 'context.core.query.message', $p->get_attribute( 'data-wp-text' ) ); + $this->assertSame( 'context.message', $p->get_attribute( 'data-wp-text' ) ); $p->next_tag( array( 'class_name' => 'wp-block-query__enhanced-pagination-animation' ) ); - $this->assertSame( 'selectors.core.query.startAnimation', $p->get_attribute( 'data-wp-class--start-animation' ) ); - $this->assertSame( 'selectors.core.query.finishAnimation', $p->get_attribute( 'data-wp-class--finish-animation' ) ); + $this->assertSame( 'state.startAnimation', $p->get_attribute( 'data-wp-class--start-animation' ) ); + $this->assertSame( 'state.finishAnimation', $p->get_attribute( 'data-wp-class--finish-animation' ) ); } /** @@ -170,7 +170,7 @@ public function test_enhanced_query_markup_rendering_at_bottom_on_custom_html_el $this->assertSame( $p->next_tag(), true ); // Test that that div is the accesibility one. $this->assertSame( 'screen-reader-text', $p->get_attribute( 'class' ) ); - $this->assertSame( 'context.core.query.message', $p->get_attribute( 'data-wp-text' ) ); + $this->assertSame( 'context.message', $p->get_attribute( 'data-wp-text' ) ); $this->assertSame( 'polite', $p->get_attribute( 'aria-live' ) ); } diff --git a/phpunit/class-wp-theme-json-test.php b/phpunit/class-wp-theme-json-test.php index 07bec961553c9a..89900d45893d91 100644 --- a/phpunit/class-wp-theme-json-test.php +++ b/phpunit/class-wp-theme-json-test.php @@ -1646,6 +1646,136 @@ public function data_sanitize_for_block_with_style_variations() { ); } + public function test_sanitize_indexed_arrays() { + $theme_json = new WP_Theme_JSON_Gutenberg( + array( + 'version' => '2', + 'badKey2' => 'I am Evil!!!!', + 'settings' => array( + 'badKey3' => 'I am Evil!!!!', + 'typography' => array( + 'badKey4' => 'I am Evil!!!!', + 'fontFamilies' => array( + 'custom' => array( + array( + 'badKey4' => 'I am Evil!!!!', + 'name' => 'Arial', + 'slug' => 'arial', + 'fontFamily' => 'Arial, sans-serif', + ), + ), + 'theme' => array( + array( + 'badKey5' => 'I am Evil!!!!', + 'name' => 'Piazzolla', + 'slug' => 'piazzolla', + 'fontFamily' => 'Piazzolla', + 'fontFace' => array( + array( + 'badKey6' => 'I am Evil!!!!', + 'fontFamily' => 'Piazzolla', + 'fontStyle' => 'italic', + 'fontWeight' => '400', + 'src' => 'https://example.com/font.ttf', + ), + array( + 'badKey7' => 'I am Evil!!!!', + 'fontFamily' => 'Piazzolla', + 'fontStyle' => 'italic', + 'fontWeight' => '400', + 'src' => 'https://example.com/font.ttf', + ), + ), + ), + array( + 'badKey8' => 'I am Evil!!!!', + 'name' => 'Inter', + 'slug' => 'Inter', + 'fontFamily' => 'Inter', + 'fontFace' => array( + array( + 'badKey9' => 'I am Evil!!!!', + 'fontFamily' => 'Inter', + 'fontStyle' => 'italic', + 'fontWeight' => '400', + 'src' => 'https://example.com/font.ttf', + ), + array( + 'badKey10' => 'I am Evil!!!!', + 'fontFamily' => 'Inter', + 'fontStyle' => 'italic', + 'fontWeight' => '400', + 'src' => 'https://example.com/font.ttf', + ), + ), + ), + ), + ), + ), + ), + ) + ); + + $expected_sanitized = array( + 'version' => '2', + 'settings' => array( + 'typography' => array( + 'fontFamilies' => array( + 'custom' => array( + array( + 'name' => 'Arial', + 'slug' => 'arial', + 'fontFamily' => 'Arial, sans-serif', + ), + ), + 'theme' => array( + array( + 'name' => 'Piazzolla', + 'slug' => 'piazzolla', + 'fontFamily' => 'Piazzolla', + 'fontFace' => array( + array( + 'fontFamily' => 'Piazzolla', + 'fontStyle' => 'italic', + 'fontWeight' => '400', + 'src' => 'https://example.com/font.ttf', + ), + array( + 'fontFamily' => 'Piazzolla', + 'fontStyle' => 'italic', + 'fontWeight' => '400', + 'src' => 'https://example.com/font.ttf', + ), + ), + ), + array( + 'name' => 'Inter', + 'slug' => 'Inter', + 'fontFamily' => 'Inter', + 'fontFace' => array( + array( + 'fontFamily' => 'Inter', + 'fontStyle' => 'italic', + 'fontWeight' => '400', + 'src' => 'https://example.com/font.ttf', + ), + array( + 'fontFamily' => 'Inter', + 'fontStyle' => 'italic', + 'fontWeight' => '400', + 'src' => 'https://example.com/font.ttf', + ), + ), + ), + ), + ), + ), + ), + ); + $sanitized_theme_json = $theme_json->get_raw_data(); + $this->assertSameSetsWithIndex( $expected_sanitized, $sanitized_theme_json, 'Sanitized theme.json does not match' ); + } + /** * @dataProvider data_sanitize_with_invalid_style_variation * diff --git a/phpunit/experimental/interactivity-api/class-wp-interactivity-store-test.php b/phpunit/experimental/interactivity-api/class-wp-interactivity-store-test.php index 22205289b20bee..837d6fd50f193a 100644 --- a/phpunit/experimental/interactivity-api/class-wp-interactivity-store-test.php +++ b/phpunit/experimental/interactivity-api/class-wp-interactivity-store-test.php @@ -161,7 +161,7 @@ public function test_store_should_be_correctly_rendered() { WP_Interactivity_Store::render(); $rendered = ob_get_clean(); $this->assertSame( - '', + '', $rendered ); } @@ -179,7 +179,7 @@ public function test_store_should_also_escape_tags_and_amps() { WP_Interactivity_Store::render(); $rendered = ob_get_clean(); $this->assertSame( - '', + '', $rendered ); } diff --git a/phpunit/experimental/modules/class-gutenberg-modules-test.php b/phpunit/experimental/modules/class-gutenberg-modules-test.php new file mode 100644 index 00000000000000..87ff6e6647d7bf --- /dev/null +++ b/phpunit/experimental/modules/class-gutenberg-modules-test.php @@ -0,0 +1,66 @@ +old_wp_scripts = isset( $GLOBALS['wp_scripts'] ) ? $GLOBALS['wp_scripts'] : null; + remove_action( 'wp_default_scripts', 'wp_default_scripts' ); + remove_action( 'wp_default_scripts', 'wp_default_packages' ); + $GLOBALS['wp_scripts'] = new WP_Scripts(); + $this->old_modules_markup = get_echo( array( 'Gutenberg_Modules', 'print_enqueued_modules' ) ); + } + + public function tear_down() { + $GLOBALS['wp_scripts'] = $this->old_wp_scripts; + add_action( 'wp_default_scripts', 'wp_default_scripts' ); + parent::tear_down(); + } + + public function test_wp_enqueue_module() { + global $wp_version; + gutenberg_register_module( 'no-deps-no-version', 'interactivity-api-1.js' ); + gutenberg_enqueue_module( 'no-deps-no-version' ); + gutenberg_register_module( 'deps-no-version', 'interactivity-api-2.js', array( 'no-deps-no-version' ) ); + gutenberg_enqueue_module( 'deps-no-version' ); + + $modules_markup = get_echo( array( 'Gutenberg_Modules', 'print_enqueued_modules' ) ); + $import_map_markup = get_echo( array( 'Gutenberg_Modules', 'print_import_map' ) ); + $preload_markup = get_echo( array( 'Gutenberg_Modules', 'print_module_preloads' ) ); + + $previous_tags = new WP_HTML_Tag_Processor( $this->old_modules_markup ); + $previous_src_stack = array(); + while ( $previous_tags->next_tag( array( 'type' => 'module' ) ) ) { + $previous_src_stack[] = $previous_tags->get_attribute( 'src' ); + } + // Test that there are 2 new `, + } ); + } + ); + + await page + .getByRole( 'toolbar', { name: 'Document tools' } ) + .getByRole( 'button', { name: 'Toggle block inserter' } ) + .click(); + + const blockLibrary = page.getByRole( 'region', { + name: 'Block Library', + } ); + const downloadableBlock = page + .getByRole( 'listbox', { + name: 'Blocks available for install', + } ) + .getByRole( 'option', { + name: `Install ${ MOCK_BLOCK1.title }.`, + exact: true, + } ); + + await blockLibrary + .getByRole( 'searchbox', { + name: 'Search for blocks and patterns', + } ) + .fill( MOCK_BLOCK1.title ); + + await expect( downloadableBlock ).toBeVisible(); + + // Install the block. + await downloadableBlock.click(); + await page + .getByRole( 'button', { name: 'Dismiss this notice' } ) + .filter( { hasText: MOCK_BLOCK1.title } ) + .waitFor(); + + await expect( + page.getByRole( 'document', { + name: `Block: ${ MOCK_BLOCK1.title }`, + } ) + ).toBeVisible(); + } ); +} ); diff --git a/test/e2e/specs/editor/plugins/block-icons.spec.js b/test/e2e/specs/editor/plugins/block-icons.spec.js new file mode 100644 index 00000000000000..0418f4200afc05 --- /dev/null +++ b/test/e2e/specs/editor/plugins/block-icons.spec.js @@ -0,0 +1,228 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +const dashIconRegex = /.*?<\/span>/; +const circleString = + ''; +const svgIcon = new RegExp( + `${ circleString }` +); + +test.describe( 'Block Icons', () => { + test.beforeAll( async ( { requestUtils } ) => { + await requestUtils.activatePlugin( 'gutenberg-test-block-icons' ); + } ); + + test.beforeEach( async ( { admin } ) => { + await admin.createNewPost(); + } ); + + test.afterAll( async ( { requestUtils } ) => { + await requestUtils.deactivatePlugin( 'gutenberg-test-block-icons' ); + } ); + + test( 'Block with svg icon', async ( { editor, page } ) => { + await page + .getByRole( 'toolbar', { name: 'Document tools' } ) + .getByRole( 'button', { name: 'Toggle block inserter' } ) + .click(); + + const blockLibrary = page.getByRole( 'region', { + name: 'Block Library', + } ); + + await blockLibrary.getByRole( 'searchbox' ).fill( 'TestSimpleSvgIcon' ); + + const blockOption = blockLibrary.getByRole( 'option', { + name: 'TestSimpleSvgIcon', + } ); + const blockIcon = blockOption.locator( '.block-editor-block-icon' ); + + // Renders correctly the icon in the inserter. + await expect.poll( () => blockIcon.innerHTML() ).toMatch( svgIcon ); + + // Can insert the block. + await blockOption.click(); + await expect( + page.getByRole( 'document', { name: 'Block: TestSimpleSvgIcon' } ) + ).toBeVisible(); + + // Renders correctly the icon on the inspector. + await editor.openDocumentSettingsSidebar(); + const inspectorIcon = page + .getByRole( 'region', { name: 'Editor settings' } ) + .locator( '.block-editor-block-icon' ); + await expect.poll( () => inspectorIcon.innerHTML() ).toMatch( svgIcon ); + } ); + + test( 'Block with dash icon', async ( { editor, page } ) => { + await page + .getByRole( 'toolbar', { name: 'Document tools' } ) + .getByRole( 'button', { name: 'Toggle block inserter' } ) + .click(); + + const blockLibrary = page.getByRole( 'region', { + name: 'Block Library', + } ); + + await blockLibrary + .getByRole( 'searchbox' ) + .fill( 'TestSimpleDashIcon' ); + + const blockOption = blockLibrary.getByRole( 'option', { + name: 'TestSimpleDashIcon', + } ); + const blockIcon = blockOption.locator( '.block-editor-block-icon' ); + + // Renders correctly the icon in the inserter. + await expect + .poll( () => blockIcon.innerHTML() ) + .toMatch( dashIconRegex ); + + // Can insert the block + await blockOption.click(); + await expect( + page.getByRole( 'document', { name: 'Block: TestSimpleDashIcon' } ) + ).toBeVisible(); + + // Renders correctly the icon on the inspector. + await editor.openDocumentSettingsSidebar(); + const inspectorIcon = page + .getByRole( 'region', { name: 'Editor settings' } ) + .locator( '.block-editor-block-icon' ); + await expect + .poll( () => inspectorIcon.innerHTML() ) + .toMatch( dashIconRegex ); + } ); + + test( 'Block with function icon', async ( { editor, page } ) => { + await page + .getByRole( 'toolbar', { name: 'Document tools' } ) + .getByRole( 'button', { name: 'Toggle block inserter' } ) + .click(); + + const blockLibrary = page.getByRole( 'region', { + name: 'Block Library', + } ); + + await blockLibrary.getByRole( 'searchbox' ).fill( 'TestFunctionIcon' ); + + const blockOption = blockLibrary.getByRole( 'option', { + name: 'TestFunctionIcon', + } ); + const blockIcon = blockOption.locator( '.block-editor-block-icon' ); + + // Renders correctly the icon in the inserter. + await expect.poll( () => blockIcon.innerHTML() ).toMatch( svgIcon ); + + // Can insert the block. + await blockOption.click(); + await expect( + page.getByRole( 'document', { name: 'Block: TestFunctionIcon' } ) + ).toBeVisible(); + + // Renders correctly the icon on the inspector. + await editor.openDocumentSettingsSidebar(); + const inspectorIcon = page + .getByRole( 'region', { name: 'Editor settings' } ) + .locator( '.block-editor-block-icon' ); + await expect.poll( () => inspectorIcon.innerHTML() ).toMatch( svgIcon ); + } ); + + test( 'Block with dash icon and background/foreground colors', async ( { + editor, + page, + } ) => { + await page + .getByRole( 'toolbar', { name: 'Document tools' } ) + .getByRole( 'button', { name: 'Toggle block inserter' } ) + .click(); + + const blockLibrary = page.getByRole( 'region', { + name: 'Block Library', + } ); + + await blockLibrary + .getByRole( 'searchbox' ) + .fill( 'TestDashIconColors' ); + + const blockOption = blockLibrary.getByRole( 'option', { + name: 'TestDashIconColors', + } ); + const blockIcon = blockOption.locator( '.block-editor-block-icon' ); + + await expect( blockIcon ).toHaveCSS( + 'background-color', + 'rgb(1, 0, 0)' + ); + await expect( blockIcon ).toHaveCSS( 'color', 'rgb(254, 0, 0)' ); + await expect + .poll( () => blockIcon.innerHTML() ) + .toMatch( dashIconRegex ); + + await blockOption.click(); + await editor.openDocumentSettingsSidebar(); + + const inspectorIcon = page + .getByRole( 'region', { name: 'Editor settings' } ) + .locator( '.block-editor-block-icon' ); + + await expect( inspectorIcon ).toHaveCSS( + 'background-color', + 'rgb(1, 0, 0)' + ); + await expect( inspectorIcon ).toHaveCSS( 'color', 'rgb(254, 0, 0)' ); + await expect + .poll( () => inspectorIcon.innerHTML() ) + .toMatch( dashIconRegex ); + } ); + + test( 'Block with svg icon and background should compute a readable foreground color', async ( { + editor, + page, + } ) => { + await page + .getByRole( 'toolbar', { name: 'Document tools' } ) + .getByRole( 'button', { name: 'Toggle block inserter' } ) + .click(); + + const blockLibrary = page.getByRole( 'region', { + name: 'Block Library', + } ); + + await blockLibrary + .getByRole( 'searchbox' ) + .fill( 'TestSvgIconBackground' ); + + const blockOption = blockLibrary.getByRole( 'option', { + name: 'TestSvgIconBackground', + } ); + const blockIcon = blockOption.locator( '.block-editor-block-icon' ); + + await expect( blockIcon ).toHaveCSS( + 'background-color', + 'rgb(1, 0, 0)' + ); + await expect( blockIcon ).toHaveCSS( 'color', 'rgb(248, 249, 249)' ); + await expect.poll( () => blockIcon.innerHTML() ).toMatch( svgIcon ); + + await blockOption.click(); + await editor.openDocumentSettingsSidebar(); + + const inspectorIcon = page + .getByRole( 'region', { name: 'Editor settings' } ) + .locator( '.block-editor-block-icon' ); + + await expect( inspectorIcon ).toHaveCSS( + 'background-color', + 'rgb(1, 0, 0)' + ); + await expect( inspectorIcon ).toHaveCSS( + 'color', + 'rgb(248, 249, 249)' + ); + await expect.poll( () => inspectorIcon.innerHTML() ).toMatch( svgIcon ); + } ); +} ); diff --git a/test/e2e/specs/editor/various/is-typing.spec.js b/test/e2e/specs/editor/various/is-typing.spec.js new file mode 100644 index 00000000000000..0cd5e0d6f64953 --- /dev/null +++ b/test/e2e/specs/editor/various/is-typing.spec.js @@ -0,0 +1,63 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +test.describe( 'isTyping', () => { + test.beforeEach( async ( { admin } ) => { + await admin.createNewPost(); + } ); + + test( 'should hide the toolbar when typing', async ( { editor, page } ) => { + // Enter to reach paragraph block. + await page.keyboard.press( 'Enter' ); + // Insert paragraph + await page.keyboard.type( 'Type' ); + + const blockToolbar = page.locator( + 'role=toolbar[name="Block tools"i]' + ); + + // Toolbar should not be showing + await expect( blockToolbar ).toBeHidden(); + + // Moving the mouse shows the toolbar. + await editor.showBlockToolbar(); + + // Toolbar is visible. + await expect( blockToolbar ).toBeVisible(); + + // Typing again hides the toolbar + await page.keyboard.type( ' and continue' ); + + // Toolbar is hidden again + await expect( blockToolbar ).toBeHidden(); + } ); + + test( 'should not close the dropdown when typing in it', async ( { + editor, + page, + } ) => { + // Add a block with a dropdown in the toolbar that contains an input. + await editor.insertBlock( { name: 'core/query' } ); + + // Tab to Start Blank Button + await page.keyboard.press( 'Tab' ); + // Select the Start Blank Button + await page.keyboard.press( 'Enter' ); + // Select the First variation + await page.keyboard.press( 'Enter' ); + // Moving the mouse shows the toolbar. + await editor.showBlockToolbar(); + // Open the dropdown. + await page.getByRole( 'button', { name: 'Display settings' } ).click(); + + const itemsPerPageInput = page.getByLabel( 'Items per Page' ); + // Make sure we're where we think we are + await expect( itemsPerPageInput ).toBeFocused(); + // Type inside the dropdown's input + await page.keyboard.type( '00' ); + // The input should still be visible. + await expect( itemsPerPageInput ).toBeVisible(); + } ); +} ); diff --git a/test/e2e/specs/interactivity/directive-effect.spec.ts b/test/e2e/specs/interactivity/directive-watch.spec.ts similarity index 79% rename from test/e2e/specs/interactivity/directive-effect.spec.ts rename to test/e2e/specs/interactivity/directive-watch.spec.ts index 40030d257661fc..09bd0214c0a51e 100644 --- a/test/e2e/specs/interactivity/directive-effect.spec.ts +++ b/test/e2e/specs/interactivity/directive-watch.spec.ts @@ -3,14 +3,14 @@ */ import { test, expect } from './fixtures'; -test.describe( 'data-wp-effect', () => { +test.describe( 'data-wp-watch', () => { test.beforeAll( async ( { interactivityUtils: utils } ) => { await utils.activatePlugins(); - await utils.addPostWithBlock( 'test/directive-effect' ); + await utils.addPostWithBlock( 'test/directive-watch' ); } ); test.beforeEach( async ( { interactivityUtils: utils, page } ) => { - await page.goto( utils.getLink( 'test/directive-effect' ) ); + await page.goto( utils.getLink( 'test/directive-watch' ) ); } ); test.afterAll( async ( { interactivityUtils: utils } ) => { @@ -18,12 +18,12 @@ test.describe( 'data-wp-effect', () => { await utils.deleteAllPosts(); } ); - test( 'check that effect runs when it is added', async ( { page } ) => { + test( 'check that watch runs when it is added', async ( { page } ) => { const el = page.getByTestId( 'element in the DOM' ); await expect( el ).toContainText( 'element is in the DOM' ); } ); - test( 'check that effect runs when it is removed', async ( { page } ) => { + test( 'check that watch runs when it is removed', async ( { page } ) => { await page.getByTestId( 'toggle' ).click(); const el = page.getByTestId( 'element in the DOM' ); await expect( el ).toContainText( 'element is not in the DOM' ); diff --git a/test/e2e/specs/interactivity/store-afterload.spec.ts b/test/e2e/specs/interactivity/store-afterload.spec.ts deleted file mode 100644 index 388e80177b0339..00000000000000 --- a/test/e2e/specs/interactivity/store-afterload.spec.ts +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Internal dependencies - */ -import { test, expect } from './fixtures'; - -test.describe( 'store afterLoad callbacks', () => { - test.beforeAll( async ( { interactivityUtils: utils } ) => { - await utils.activatePlugins(); - await utils.addPostWithBlock( 'test/store-afterload' ); - } ); - - test.beforeEach( async ( { interactivityUtils: utils, page } ) => { - await page.goto( utils.getLink( 'test/store-afterload' ) ); - } ); - - test.afterAll( async ( { interactivityUtils: utils } ) => { - await utils.deactivatePlugins(); - await utils.deleteAllPosts(); - } ); - - test( 'run after the vdom and store are ready', async ( { page } ) => { - const allStoresReady = page.getByTestId( 'all-stores-ready' ); - const vdomReady = page.getByTestId( 'vdom-ready' ); - - await expect( allStoresReady ).toHaveText( 'true' ); - await expect( vdomReady ).toHaveText( 'true' ); - } ); - - test( 'run once even if shared between several store calls', async ( { - page, - } ) => { - const afterLoadTimes = page.getByTestId( 'after-load-exec-times' ); - const sharedAfterLoadTimes = page.getByTestId( - 'shared-after-load-exec-times' - ); - - await expect( afterLoadTimes ).toHaveText( '1' ); - await expect( sharedAfterLoadTimes ).toHaveText( '1' ); - } ); -} ); diff --git a/test/e2e/specs/site-editor/new-templates-list.spec.js b/test/e2e/specs/site-editor/new-templates-list.spec.js new file mode 100644 index 00000000000000..be6008080200c8 --- /dev/null +++ b/test/e2e/specs/site-editor/new-templates-list.spec.js @@ -0,0 +1,86 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +test.describe( 'Templates', () => { + test.beforeAll( async ( { requestUtils } ) => { + await Promise.all( [ + requestUtils.activateTheme( 'emptytheme' ), + requestUtils.activatePlugin( 'gutenberg-test-dataviews' ), + ] ); + } ); + test.afterAll( async ( { requestUtils } ) => { + await Promise.all( [ + requestUtils.activateTheme( 'twentytwentyone' ), + requestUtils.deactivatePlugin( 'gutenberg-test-dataviews' ), + ] ); + } ); + test( 'Sorting', async ( { admin, page } ) => { + await admin.visitSiteEditor( { path: '/wp_template/all' } ); + // Descending by title. + await page.getByRole( 'button', { name: 'Template' } ).click(); + await page.getByRole( 'menuitem', { name: 'Sort descending' } ).click(); + const firstTitle = page + .getByRole( 'region', { + name: 'Template', + includeHidden: true, + } ) + .getByRole( 'heading', { + level: 3, + includeHidden: true, + } ) + .first(); + await expect( firstTitle ).toHaveText( 'Tag Archives' ); + // Ascending by title. + await page.getByRole( 'menuitem', { name: 'Sort ascending' } ).click(); + await expect( firstTitle ).toHaveText( 'Category Archives' ); + } ); + test( 'Filtering', async ( { requestUtils, admin, page } ) => { + await requestUtils.createTemplate( 'wp_template', { + slug: 'date', + title: 'Date Archives', + content: 'hi', + } ); + await admin.visitSiteEditor( { path: '/wp_template/all' } ); + // Global search. + await page.getByRole( 'searchbox', { name: 'Filter list' } ).click(); + await page.keyboard.type( 'tag' ); + const titles = page + .getByRole( 'region', { name: 'Template' } ) + .getByRole( 'heading', { level: 3 } ); + await expect( titles ).toHaveCount( 1 ); + await expect( titles.first() ).toHaveText( 'Tag Archives' ); + await page.getByRole( 'button', { name: 'Reset filters' } ).click(); + await expect( titles ).toHaveCount( 6 ); + + // Filter by author. + await page.getByRole( 'button', { name: 'Add filter' } ).click(); + await page.getByRole( 'menuitem', { name: 'Author' } ).hover(); + await page.getByRole( 'menuitemcheckbox', { name: 'admin' } ).click(); + await expect( titles ).toHaveCount( 1 ); + await expect( titles.first() ).toHaveText( 'Date Archives' ); + + // Filter by author and text. + await page.getByRole( 'button', { name: 'Reset filters' } ).click(); + await page.getByRole( 'searchbox', { name: 'Filter list' } ).click(); + await page.keyboard.type( 'archives' ); + await expect( titles ).toHaveCount( 3 ); + await page.getByRole( 'button', { name: 'Add filter' } ).click(); + await page.getByRole( 'menuitem', { name: 'Author' } ).hover(); + await page + .getByRole( 'menuitemcheckbox', { name: 'Emptytheme' } ) + .click(); + await expect( titles ).toHaveCount( 2 ); + + await requestUtils.deleteAllTemplates( 'wp_template' ); + } ); + test( 'Field visibility', async ( { admin, page } ) => { + await admin.visitSiteEditor( { path: '/wp_template/all' } ); + await page.getByRole( 'button', { name: 'Description' } ).click(); + await page.getByRole( 'menuitem', { name: 'Hide' } ).click(); + await expect( + page.getByRole( 'button', { name: 'Description' } ) + ).toBeHidden(); + } ); +} ); diff --git a/test/integration/blocks-raw-handling.test.js b/test/integration/blocks-raw-handling.test.js index 229fa0ba7761c8..8acfb052436ed7 100644 --- a/test/integration/blocks-raw-handling.test.js +++ b/test/integration/blocks-raw-handling.test.js @@ -369,6 +369,34 @@ describe( 'Blocks raw handling', () => { expect( console ).toHaveLogged(); } ); + it( 'should convert pre', () => { + const transformed = pasteHandler( { + HTML: '
1\n2
', + plainText: '1\n2', + } ) + .map( getBlockContent ) + .join( '' ); + + expect( transformed ).toBe( + '
1\n2
' + ); + expect( console ).toHaveLogged(); + } ); + + it( 'should convert code', () => { + const transformed = pasteHandler( { + HTML: '
1\n2
', + plainText: '1\n2', + } ) + .map( getBlockContent ) + .join( '' ); + + expect( transformed ).toBe( + '
1\n2
' + ); + expect( console ).toHaveLogged(); + } ); + describe( 'pasteHandler', () => { [ 'plain', diff --git a/test/integration/fixtures/blocks/core__social-link-gravatar.html b/test/integration/fixtures/blocks/core__social-link-gravatar.html new file mode 100644 index 00000000000000..c4137b8a083176 --- /dev/null +++ b/test/integration/fixtures/blocks/core__social-link-gravatar.html @@ -0,0 +1 @@ + diff --git a/test/integration/fixtures/blocks/core__social-link-gravatar.json b/test/integration/fixtures/blocks/core__social-link-gravatar.json new file mode 100644 index 00000000000000..2f4035d97640b2 --- /dev/null +++ b/test/integration/fixtures/blocks/core__social-link-gravatar.json @@ -0,0 +1,11 @@ +[ + { + "name": "core/social-link", + "isValid": true, + "attributes": { + "url": "https://example.com/", + "service": "gravatar" + }, + "innerBlocks": [] + } +] diff --git a/test/integration/fixtures/blocks/core__social-link-gravatar.parsed.json b/test/integration/fixtures/blocks/core__social-link-gravatar.parsed.json new file mode 100644 index 00000000000000..b4c7a8c146e142 --- /dev/null +++ b/test/integration/fixtures/blocks/core__social-link-gravatar.parsed.json @@ -0,0 +1,11 @@ +[ + { + "blockName": "core/social-link-gravatar", + "attrs": { + "url": "https://example.com/" + }, + "innerBlocks": [], + "innerHTML": "", + "innerContent": [] + } +] diff --git a/test/integration/fixtures/blocks/core__social-link-gravatar.serialized.html b/test/integration/fixtures/blocks/core__social-link-gravatar.serialized.html new file mode 100644 index 00000000000000..83a449d4e1f53c --- /dev/null +++ b/test/integration/fixtures/blocks/core__social-link-gravatar.serialized.html @@ -0,0 +1 @@ + diff --git a/test/native/integration-test-helpers/add-block.js b/test/native/integration-test-helpers/add-block.js index 5a15cb59fc6e16..eded603829c48a 100644 --- a/test/native/integration-test-helpers/add-block.js +++ b/test/native/integration-test-helpers/add-block.js @@ -6,7 +6,7 @@ import { Platform } from '@wordpress/element'; /** * External dependencies */ -import { act, fireEvent } from '@testing-library/react-native'; +import { act, fireEvent, within } from '@testing-library/react-native'; import { AccessibilityInfo } from 'react-native'; /** @@ -31,9 +31,9 @@ export const addBlock = async ( fireEvent.press( screen.getByLabelText( 'Add block' ) ); } - const blockList = screen.getByTestId( 'InserterUI-Blocks' ); + const inserterModal = screen.getByTestId( 'InserterUI-Blocks' ); // onScroll event used to force the FlatList to render all items - fireEvent.scroll( blockList, { + fireEvent.scroll( inserterModal, { nativeEvent: { contentOffset: { y: 0, x: 0 }, contentSize: { width: 100, height: 100 }, @@ -41,7 +41,7 @@ export const addBlock = async ( }, } ); - const blockButton = await screen.findByText( blockName ); + const blockButton = await within( inserterModal ).findByText( blockName ); // Blocks can perform belated state updates after they are inserted. // To avoid potential `act` warnings, we ensure that all timers and queued // microtasks are executed. diff --git a/tools/webpack/interactivity.js b/tools/webpack/interactivity.js index 26e49966ad40c3..03bb1f576cb783 100644 --- a/tools/webpack/interactivity.js +++ b/tools/webpack/interactivity.js @@ -2,28 +2,42 @@ * External dependencies */ const { join } = require( 'path' ); +const CopyWebpackPlugin = require( 'copy-webpack-plugin' ); /** * Internal dependencies */ -const { baseConfig } = require( './shared' ); +const { baseConfig, plugins } = require( './shared' ); module.exports = { ...baseConfig, name: 'interactivity', entry: { - index: { - import: `./packages/interactivity/src/index.js`, - library: { - name: [ 'wp', 'interactivity' ], - type: 'window', - }, - }, + index: `./packages/interactivity/src/index.js`, + navigation: './packages/block-library/src/navigation/view.js', + query: './packages/block-library/src/query/view.js', + image: './packages/block-library/src/image/view.js', + file: './packages/block-library/src/file/view.js', + search: './packages/block-library/src/search/view.js', + }, + experiments: { + outputModule: true, }, output: { devtoolNamespace: 'wp', filename: './build/interactivity/[name].min.js', + library: { + type: 'module', + }, path: join( __dirname, '..', '..' ), + environment: { module: true }, + }, + externalsType: 'module', + externals: { + '@wordpress/interactivity': '@wordpress/interactivity', + }, + resolve: { + extensions: [ '.js', '.ts', '.tsx' ], }, module: { rules: [ @@ -39,6 +53,7 @@ module.exports = { babelrc: false, configFile: false, presets: [ + '@babel/preset-typescript', [ '@babel/preset-react', { @@ -53,6 +68,18 @@ module.exports = { }, ], }, + plugins: [ + ...plugins, + // TODO: Move it to a different Webpack file. + new CopyWebpackPlugin( { + patterns: [ + { + from: './node_modules/es-module-shims/dist/es-module-shims.wasm.js', + to: './build/modules/importmap-polyfill.min.js', + }, + ], + } ), + ], watchOptions: { ignored: [ '**/node_modules' ], aggregateTimeout: 500, diff --git a/tsconfig.json b/tsconfig.json index 4ee1787a247cf7..d05e883ed70b03 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -27,6 +27,7 @@ { "path": "packages/html-entities" }, { "path": "packages/i18n" }, { "path": "packages/icons" }, + { "path": "packages/interactivity" }, { "path": "packages/is-shallow-equal" }, { "path": "packages/keycodes" }, { "path": "packages/lazy-import" },