diff --git a/.github/workflows/end2end-test.yml b/.github/workflows/end2end-test.yml index 777549e334aa96..5a9750c6bb0456 100644 --- a/.github/workflows/end2end-test.yml +++ b/.github/workflows/end2end-test.yml @@ -88,6 +88,8 @@ jobs: npm run wp-env start - name: Run the tests + env: + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 run: | xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run test:e2e:playwright -- --shard=${{ matrix.part }}/${{ matrix.totalParts }} diff --git a/.github/workflows/stale-issue-gardening.yml b/.github/workflows/stale-issue-gardening.yml index 0bdb1cfbf0cefd..cbeb04ead53214 100644 --- a/.github/workflows/stale-issue-gardening.yml +++ b/.github/workflows/stale-issue-gardening.yml @@ -27,8 +27,8 @@ jobs: remove-stale-when-updated: true stale-issue-label: '[Status] Stale' - name: 'Flaky test issues without activity' - message: 'This issue has gone 30 days without any activity.' - days-before-stale: 30 + message: 'This issue has gone 15 days without any activity.' + days-before-stale: 15 days-before-close: 1 only-labels: '[Type] Flaky Test' remove-stale-when-updated: true diff --git a/bin/plugin/commands/performance.js b/bin/plugin/commands/performance.js index 4be675a0a5d40d..bdc38347e40c86 100644 --- a/bin/plugin/commands/performance.js +++ b/bin/plugin/commands/performance.js @@ -87,6 +87,7 @@ async function runTestSuite( testSuite, testRunnerDir, runKey ) { testRunnerDir, { ...process.env, + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1', WP_ARTIFACTS_PATH: ARTIFACTS_PATH, RESULTS_ID: runKey, } diff --git a/changelog.txt b/changelog.txt index 1814106e5e5092..92fa2690b15d68 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,328 @@ == 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 = + + +## Changelog + +### Bug fixes + +#### Components +- https://github.com/WordPress/gutenberg/pull/56570 + + + = 17.1.2 = ## Changelog 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/file-structure-of-a-block.md b/docs/getting-started/fundamentals/file-structure-of-a-block.md new file mode 100644 index 00000000000000..130483ae5af70f --- /dev/null +++ b/docs/getting-started/fundamentals/file-structure-of-a-block.md @@ -0,0 +1,86 @@ +# 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](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 `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") + +### `.php` + +A block is usually added to the block editor using a WordPress plugin. In the main PHP file of the plugin the block is usually registered on the server side. + +
+For more on creating a WordPress plugin see Plugin Basics, and Plugin Header requirements for explanation and additional fields you can include in your plugin header. +
+ +### `package.json` + +[`package.json`](https://docs.npmjs.com/cli/v10/configuring-npm/package-json) is a configuration file for a Node.js project. In this file you define the NPM dependencies of the block and the scripts used for local work. + +### `src` folder + +In a standard project you'll place your block files in the `src` folder. By default, the build process with `wp-scripts `will take files from this folder and will generate the bundled files in the `build` folder. + +### `block.json` + +This file contains the metadata of the block, and it's used to simplify the definition and registration of the block both in the client and on the server. + +Among other data it provides properties to define the paths of the files involved in the block's behaviour, output and style. If there's a build process involved, this `block.json` along with the generated files are placed into a destination folder (usually the `build` folder) so the paths provided target to the bundled versions of these files. + +The most relevant properties that can be defined in a `block.json` to set the files involved in the block's behaviour, output or style are: +- The `editorScript` property, usually set with the path of a bundled `index.js` file (output build from `src/index.js`). +- The `style` property, usually set with the path of a bundled `style-index.css` file (output build from `src/style.(css|scss|sass)`). +- The `editorStyle` property, usually set with the path of a bundled `index.css` (output build from `src/editor.(css|scss|sass)`). +- The `render` property, usually set with the path of a bundled `render.php` (output copied from `src/render.php`). +- The `viewScript` property, usually set with the path of a bundled `view.js` (output copied from `src/view.php`). + +[![Open Build Output Diagram in excalidraw](https://developer.wordpress.org/files/2023/11/file-structure-build-output.png)](https://excalidraw.com/#json=c22LROgcG4JkD-7SkuE-N,rQW_ViJBq0Yk3qhCgqD6zQ "Open Build Output Diagram in excalidraw") + +### `index.js` + +The `index.js` file (or any other file defined in the `editorScript` property of `block.json`) is the entry point file for javascript that should only get loaded in the editor. It is responsible for calling the `registerBlockType` function to register the block on the client. In a standard structure it imports the `edit.js` and `save.js` files to get functions required in block registration. + +### `edit.js` + +The `edit.js` commonly gets used to contain the React component that gets used in the editor for our block. It usually exports a single component that then gets passed to the `edit` property of the `registerBlockType` function in the `index.js` file. + +### `save.js` + +The `save.js` exports the function that returns the static HTML markup that gets saved to the Database. + +### `style.(css|scss|sass)` + +A `style` file with any of the extensions `.css`, `.scss` or `.sass`, contains the styles of the block that will be loaded in both the editor and the frontend. In the build process this file is converted into `style-index.css` which is usually defined at `style` property in `block.json` + +
+ The webpack config used internally by wp-scripts includes a css-loader chained with postcss-loader and sass-loader that allows it to process CSS, SASS or SCSS files. Check Default webpack config for more info +
+ + +### `editor.(css|scss|sass)` + +An `editor` file with any of the extensions `.css`, `.scss` or `.sass`, contains the additional styles applied to the block only in the editor’s context. In the build process this file is converted into `index.css` which is usually defined at `editorStyle` property in `block.json` + +### `render.php` + +The `render.php` file (or any other file defined in the `render` property of `block.json`) defines the server side process that returns the markup for the block when there is a request from the frontend. If this file is defined, it will take precedence over any other ways to render the block's markup for the frontend. + +### `view.js` + +The `view.js` file (or any other file defined in the `viewScript` property of `block.json`) will be loaded in the front-end when the block is displayed. + +### `build` folder + +In a standard project, the `build` folder contains the generated files in the build process triggered by the `build` or `start` commands of `wp-scripts`. + +
+ You can use webpack-src-dir and output-path option of wp-scripts build commands to customize the entry and output points +
+ +## Additional resources + +- [Metadata in block.json](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-metadata/) +- [`wp-scripts build`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-scripts/#build) +- [`wp-scripts start`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-scripts/#start) +- [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/javascript-in-the-block-editor.md b/docs/getting-started/fundamentals/javascript-in-the-block-editor.md new file mode 100644 index 00000000000000..73c6a6c56e6328 --- /dev/null +++ b/docs/getting-started/fundamentals/javascript-in-the-block-editor.md @@ -0,0 +1,51 @@ +# Working with Javascript for the Block Editor + +A JavaScript Build Process is recommended for most cases when working with Javascript for the Block Editor. With a build process, you'll be able to work with ESNext and JSX (among others) syntaxes and features in your code while producing code ready for the majority of the browsers. + +## JavaScript Build Process + +["ESNext"](https://developer.mozilla.org/en-US/docs/Web/JavaScript/JavaScript_technologies_overview#standardization_process) is a dynamic name that refers to Javascript's latest syntax and features. ["JSX"](https://react.dev/learn/writing-markup-with-jsx) is a custom syntax extension to JavaScript, created by React project, that allows you to write JavaScript using a familiar HTML tag-like syntax. + +Browsers cannot interpret or run ESNext and JSX syntaxes, so a transformation step is needed to convert these syntaxes to code that browsers can understand. + +["webpack"](https://webpack.js.org/concepts/why-webpack/) is a pluggable tool that processes JavaScript and creates a compiled bundle that runs in a browser. ["babel"](https://babeljs.io/) transforms JavaScript from one format to another. Babel is a webpack plugin to transform ESNext and JSX to production-ready JavaScript. + +[`@wordpress/scripts`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-scripts/) package abstracts these libraries away to standardize and simplify development, so you won’t need to handle the details for configuring webpack or babel. Check the [Get started with wp-scripts](https://developer.wordpress.org/block-editor/getting-started/devenv/get-started-with-wp-scripts/) intro guide. + + +Among other things, with `wp-scripts` package you can use Javascript modules to distribute your code among different files and get a few bundled files at the end of the build process (see [example](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/data-basics-59c8f8)). + +[![Build Process Diagram](https://developer.wordpress.org/files/2023/11/build-process.png)](https://excalidraw.com/#json=4aNG9JUti3pMnsfoga35b,ihEAI8p5dwkpjWr6gQmjuw "Open Build Process Diagram in Excalidraw") + +With the [proper `package.json` scripts](https://developer.wordpress.org/block-editor/getting-started/devenv/get-started-with-wp-scripts/#basic-usage) you can launch the build process with `wp-scripts` in production and development mode: + +- **`npm run build` for "production" mode build** - This process [minifies the code](https://developer.mozilla.org/en-US/docs/Glossary/Minification) so it downloads faster in the browser. +- **`npm run start` for "development" mode build** - This process does not minify the code of the bundled files, provides [source maps files](https://firefox-source-docs.mozilla.org/devtools-user/debugger/how_to/use_a_source_map/index.html) for them, and additionally continues a running process to watch the source file for more changes and rebuilds as you develop. + +
+ You can provide your own custom webpack.config.js to wp-scripts to customize the build process to suit your needs +
+ +## Javascript without a build process + +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 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)). + +
+ Try running wp.data.select('core/editor').getBlocks()) in your browser's dev tools while editing a post or a site. The entire editor is available from the console. +
+ +Use [`enqueue_block_editor_assets`](https://developer.wordpress.org/reference/hooks/enqueue_block_editor_assets/) hook coupled with the standard [`wp_enqueue_script`](https://developer.wordpress.org/reference/functions/wp_enqueue_script/) (and [`wp_register_script`](https://developer.wordpress.org/reference/functions/wp_register_script/)) to enqueue javascript assets for the Editor with access to these packages via `wp` (see [example](https://github.com/wptrainingteam/block-theme-examples/tree/master/example-block-variation)). Refer to [Enqueueing assets in the Editor](https://developer.wordpress.org/block-editor/how-to-guides/enqueueing-assets-in-the-editor/) for more info. + +## 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 +- [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/registration-of-a-block.md b/docs/getting-started/fundamentals/registration-of-a-block.md new file mode 100644 index 00000000000000..7cc8e6bcbe8b06 --- /dev/null +++ b/docs/getting-started/fundamentals/registration-of-a-block.md @@ -0,0 +1,98 @@ +# Registration of a block + +A block is usually registered through a plugin on both the server and client-side using its `block.json` metadata. + +Although technically, blocks could be registered only in the client, **registering blocks on both the server and in the client is a strong recommendation**. Some server-side features like Dynamic Rendering, Block Supports, Block Hooks, or Block style variations require the block to "exist" on the server, and they won't work properly without server registration of the block. + +For example, to allow a block [to be styled via `theme.json`](https://developer.wordpress.org/themes/global-settings-and-styles/settings/blocks/), it needs to be registered on the server, otherwise, any styles assigned to it in `theme.json` will be ignored. + +[![Open Block Registration diagram in excalidraw](https://developer.wordpress.org/files/2023/11/block-registration-e1700493399839.png)](https://excalidraw.com/#json=PUQu7jpvbKsUHYfpHWn7s,61QnhpZtjykp3s44lbUN_g "Open Block Registration diagram in excalidraw") + +### Registration of the block with PHP (server-side) + +Block registration on the server usually takes place in the main plugin PHP file with the `register_block_type` function called on the [init hook](https://developer.wordpress.org/reference/hooks/init/). + +The [`register_block_type`](https://developer.wordpress.org/reference/functions/register_block_type/) function aims to simplify block type registration on the server by reading metadata stored in the `block.json` file. + +This function takes two params relevant in this context (`$block_type` accepts more types and variants): + +- `$block_type` (`string`) – path to the folder where the `block.json` file is located or full path to the metadata file if named differently. +- `$args` (`array`) – an optional array of block type arguments. Default value: `[]`. Any arguments may be defined. However, the one described below is supported by default: + - `$render_callback` (`callable`) – callback used to render blocks of this block type, it's an alternative to the `render` field in `block.json`. + +As part of the build process, the `block.json` file is usually copied from the `src` folder to the `build` folder, so the path to the `block.json` of your registered block should refer to the `build` folder. + +`register_block_type` returns the registered block type (`WP_Block_Type`) on success or `false` on failure. + +**Example:** +```php +register_block_type( + __DIR__ . '/notice', + array( + 'render_callback' => 'render_block_core_notice', + ) +); +``` + +**Example:** +```php +function minimal_block_ca6eda___register_block() { + register_block_type( __DIR__ . '/build' ); +} + +add_action( 'init', 'minimal_block_ca6eda___register_block' ); +``` +_See the [full block example](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/minimal-block-ca6eda) of the [code above](https://github.com/WordPress/block-development-examples/blob/trunk/plugins/minimal-block-ca6eda/index.php)_ + +### Registration of the block with JavaScript (client-side) + +When the block is registered on the server, you only need to register the client-side settings on the client using the same block’s name. + +**Example:** + +```js +registerBlockType( 'my-plugin/notice', { + edit: Edit, + // ...other client-side settings +} ); +``` + +Although registering the block also on the server with PHP is still recommended for the reasons mentioned at ["Benefits using the metadata file"](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-metadata/#benefits-using-the-metadata-file), if you want to register it only client-side you can use [`registerBlockType`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-blocks/#registerblocktype) method from `@wordpress/blocks` package to register a block type using the metadata loaded from `block.json` file. + +The function takes two params: + +- `$blockNameOrMetadata` (`string`|`Object`) – block type name or the metadata object loaded from the `block.json` +- `$settings` (`Object`) – client-side block settings. + +
+The content of block.json (or any other .json file) can be imported directly in Javascript files when using a build process like the one available with wp-scripts +
+ +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 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. + +**Example:** + +```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: 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)_ + +## Additional resources + +- [`register_block_type` PHP function](https://developer.wordpress.org/reference/functions/register_block_type/) +- [`registerBlockType` JS function](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-blocks/#registerblocktype) +- [Why a block needs to be registered in both the server and the client?](https://github.com/WordPress/gutenberg/discussions/55884) | GitHub Discussion \ No newline at end of file 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 f380683f39ccdd..edc61d138128e6 100644 --- a/docs/reference-guides/block-api/block-metadata.md +++ b/docs/reference-guides/block-api/block-metadata.md @@ -77,65 +77,9 @@ Development is improved by using a defined schema definition file. Supported edi "$schema": "https://schemas.wp.org/trunk/block.json" ``` -## Block registration - -### PHP (server-side) - -The [`register_block_type`](https://developer.wordpress.org/reference/functions/register_block_type/) function that aims to simplify the block type registration on the server, can read metadata stored in the `block.json` file. - -This function takes two params relevant in this context (`$block_type` accepts more types and variants): - -- `$block_type` (`string`) – path to the folder where the `block.json` file is located or full path to the metadata file if named differently. -- `$args` (`array`) – an optional array of block type arguments. Default value: `[]`. Any arguments may be defined. However, the one described below is supported by default: - - `$render_callback` (`callable`) – callback used to render blocks of this block type, it's an alternative to the `render` field in `block.json`. - -It returns the registered block type (`WP_Block_Type`) on success or `false` on failure. - -**Example:** - -```php -register_block_type( - __DIR__ . '/notice', - array( - 'render_callback' => 'render_block_core_notice', - ) -); -``` - -### JavaScript (client-side) - -When the block is registered on the server, you only need to register the client-side settings on the client using the same block’s name. - -**Example:** - -```js -registerBlockType( 'my-plugin/notice', { - edit: Edit, - // ...other client-side settings -} ); -``` - -Although registering the block also on the server with PHP is still recommended for the reasons above, if you want to register it only client-side you can now use `registerBlockType` method from `@wordpress/blocks` package to register a block type using the metadata loaded from `block.json` file. - -The function takes two params: - -- `$blockNameOrMetadata` (`string`|`Object`) – block type name (supported previously) or the metadata object loaded from the `block.json` file with a bundler (e.g., webpack) or a custom Babel plugin. -- `$settings` (`Object`) – client-side block settings. - -It returns the registered block type (`WPBlock`) on success or `undefined` on failure. - -**Example:** - -```js -import { registerBlockType } from '@wordpress/blocks'; -import Edit from './edit'; -import metadata from './block.json'; - -registerBlockType( metadata, { - edit: Edit, - // ...other client-side settings -} ); -``` +
+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/block-api/block-supports.md b/docs/reference-guides/block-api/block-supports.md index a58c56d7a8a94f..7fd0e68c9bd8c0 100644 --- a/docs/reference-guides/block-api/block-supports.md +++ b/docs/reference-guides/block-api/block-supports.md @@ -556,6 +556,7 @@ supports: { - `allowVerticalAlignment`: type `boolean`, default value `true` - `allowJustification`: type `boolean`, default value `true` - `allowOrientation`: type `boolean`, default value `true` + - `allowCustomContentAndWideSize`: type `boolean`, default value `true` This value only applies to blocks that are containers for inner blocks. If set to `true` the layout type will be `flow`. For other layout types it's necessary to set the `type` explicitly inside the `default` object. @@ -615,6 +616,13 @@ For the `flex` layout type, determines display of the justification control in t For the `flex` layout type only, determines display of the orientation control in the block toolbar. +### layout.allowCustomContentAndWideSize + +- Type: `boolean` +- Default value: `true` + +For the `constrained` layout type only, determines display of the custom content and wide size controls in the block sidebar. + ## multiple 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-editor.md b/docs/reference-guides/data/data-core-editor.md index 4774934651b139..0bfa052cf15229 100644 --- a/docs/reference-guides/data/data-core-editor.md +++ b/docs/reference-guides/data/data-core-editor.md @@ -547,10 +547,6 @@ Returns state object prior to a specified optimist transaction ID, or `null` if Returns a suggested post format for the current post, inferred only if there is a single block within the post and it is of a type known to match a default post format. Returns null if the format cannot be determined. -_Parameters_ - -- _state_ `Object`: Global application state. - _Returns_ - `?string`: Suggested post format. 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/reference-guides/theme-json-reference/theme-json-living.md b/docs/reference-guides/theme-json-reference/theme-json-living.md index 492cdaf089cc4b..24a5845381bfda 100644 --- a/docs/reference-guides/theme-json-reference/theme-json-living.md +++ b/docs/reference-guides/theme-json-reference/theme-json-living.md @@ -129,6 +129,7 @@ Settings related to layout. | contentSize | string | | | | wideSize | string | | | | allowEditing | boolean | true | | +| allowCustomContentAndWideSize | boolean | true | | --- 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/gutenberg.php b/gutenberg.php index 5adc8d03a33323..2526aac3770548 100644 --- a/gutenberg.php +++ b/gutenberg.php @@ -5,7 +5,7 @@ * Description: Printing since 1440. This is the development plugin for the block editor, site editor, and other future WordPress core functionality. * Requires at least: 6.3 * Requires PHP: 7.0 - * Version: 17.1.2 + * Version: 17.2.0-rc.1 * Author: Gutenberg Team * Text Domain: gutenberg * diff --git a/lib/class-wp-theme-json-gutenberg.php b/lib/class-wp-theme-json-gutenberg.php index 8c2857fa89d0ca..9311001f2edd14 100644 --- a/lib/class-wp-theme-json-gutenberg.php +++ b/lib/class-wp-theme-json-gutenberg.php @@ -383,9 +383,10 @@ class WP_Theme_JSON_Gutenberg { 'minHeight' => null, ), 'layout' => array( - 'contentSize' => null, - 'wideSize' => null, - 'allowEditing' => null, + 'contentSize' => null, + 'wideSize' => null, + 'allowEditing' => null, + 'allowCustomContentAndWideSize' => null, ), 'lightbox' => array( 'enabled' => null, @@ -425,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. * @@ -549,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. * @@ -790,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 ) { @@ -966,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 ( empty( $tree[ $key ] ) ) { - unset( $tree[ $key ] ); + 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 ] ); + } + } 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 ] ); @@ -987,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/fonts/font-library/class-wp-font-family-utils.php b/lib/experimental/fonts/font-library/class-wp-font-family-utils.php index 8a8ee1d4ddb5f1..7d954e79e96a3c 100644 --- a/lib/experimental/fonts/font-library/class-wp-font-family-utils.php +++ b/lib/experimental/fonts/font-library/class-wp-font-family-utils.php @@ -90,4 +90,36 @@ public static function has_font_mime_type( $filepath ) { return in_array( $filetype['type'], $allowed_mime_types, true ); } + + /** + * Format font family to make it valid CSS. + * + * @since 6.5.0 + * + * @param string $font_family Font family attribute. + * @return string The formatted font family attribute. + */ + public static function format_font_family( $font_family ) { + if ( $font_family ) { + $font_families = explode( ',', $font_family ); + $wrapped_font_families = array_map( + function ( $family ) { + $trimmed = trim( $family ); + if ( ! empty( $trimmed ) && strpos( $trimmed, ' ' ) !== false && strpos( $trimmed, "'" ) === false && strpos( $trimmed, '"' ) === false ) { + return "'" . $trimmed . "'"; + } + return $trimmed; + }, + $font_families + ); + + if ( count( $wrapped_font_families ) === 1 ) { + $font_family = $wrapped_font_families[0]; + } else { + $font_family = implode( ', ', $wrapped_font_families ); + } + } + + return $font_family; + } } diff --git a/lib/experimental/fonts/font-library/class-wp-font-family.php b/lib/experimental/fonts/font-library/class-wp-font-family.php index 309b1c8b7902b5..58d4f476e834d1 100644 --- a/lib/experimental/fonts/font-library/class-wp-font-family.php +++ b/lib/experimental/fonts/font-library/class-wp-font-family.php @@ -308,6 +308,7 @@ private function sanitize() { ), ), ); + // Creates a new WP_Theme_JSON object with the new fonts to // leverage sanitization and validation. $fonts_json = WP_Theme_JSON_Gutenberg::remove_insecure_properties( $fonts_json ); @@ -316,7 +317,10 @@ private function sanitize() { $sanitized_font = ! empty( $theme_data['settings']['typography']['fontFamilies'] ) ? $theme_data['settings']['typography']['fontFamilies'][0] : array(); - $this->data = $sanitized_font; + + $sanitized_font['slug'] = _wp_to_kebab_case( $sanitized_font['slug'] ); + $sanitized_font['fontFamily'] = WP_Font_Family_Utils::format_font_family( $sanitized_font['fontFamily'] ); + $this->data = $sanitized_font; return $this->data; } 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__/align-hook.test.js.snap b/packages/e2e-tests/specs/editor/plugins/__snapshots__/align-hook.test.js.snap deleted file mode 100644 index 6c04d30c41be9d..00000000000000 --- a/packages/e2e-tests/specs/editor/plugins/__snapshots__/align-hook.test.js.snap +++ /dev/null @@ -1,43 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Align Hook Works As Expected Block with align array Correctly applies the selected alignment and correctly removes the alignment 1`] = ` -" -
Test Align Hook
-" -`; - -exports[`Align Hook Works As Expected Block with align array Correctly applies the selected alignment and correctly removes the alignment 2`] = ` -" -
Test Align Hook
-" -`; - -exports[`Align Hook Works As Expected Block with align true Correctly applies the selected alignment and correctly removes the alignment 1`] = ` -" -
Test Align Hook
-" -`; - -exports[`Align Hook Works As Expected Block with align true Correctly applies the selected alignment and correctly removes the alignment 2`] = ` -" -
Test Align Hook
-" -`; - -exports[`Align Hook Works As Expected Block with default align Correctly applies the selected alignment and correctly removes the alignment 1`] = ` -" -
Test Align Hook
-" -`; - -exports[`Align Hook Works As Expected Block with default align Correctly applies the selected alignment and correctly removes the alignment 2`] = ` -" -
Test Align Hook
-" -`; - -exports[`Align Hook Works As Expected Block with no alignment set Does not save any alignment related attribute or class 1`] = ` -" -
Test Align Hook
-" -`; 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/align-hook.test.js b/packages/e2e-tests/specs/editor/plugins/align-hook.test.js deleted file mode 100644 index 9246144d810e08..00000000000000 --- a/packages/e2e-tests/specs/editor/plugins/align-hook.test.js +++ /dev/null @@ -1,234 +0,0 @@ -/** - * WordPress dependencies - */ -import { - activatePlugin, - createNewPost, - deactivatePlugin, - getAllBlocks, - getEditedPostContent, - insertBlock, - selectBlockByClientId, - setPostContent, - clickBlockToolbarButton, -} from '@wordpress/e2e-test-utils'; - -const alignLabels = { - none: 'None', - left: 'Align left', - center: 'Align center', - right: 'Align right', - wide: 'Wide width', - full: 'Full width', -}; - -/** - * Helper function to get the `labels` of align control. It actually evaluates the - * basic label of the button without the `info` text node if exists. - * - * @param {Object} options Options for the util function - * @param {boolean} [options.getActiveButtonLabels=false] Flag for returning the active buttons labels only. - * @return {string[]} The matched labels. - */ -const getAlignmentToolbarLabels = async ( { - getActiveButtonLabels = false, -} = {} ) => { - const selector = `.components-dropdown-menu__menu button${ - getActiveButtonLabels ? '.is-active' : '' - } .components-menu-item__item`; - return page.evaluate( ( _selector ) => { - return ( - Array.from( document.querySelectorAll( _selector ) ) - /** - * We neede this for now because conditionally there could be two nodes - * with the same class(). This should be removed when the following - * issue is resolved. - * - * @see https://github.com/WordPress/gutenberg/issues/34838 - */ - .filter( ( contentNode ) => ! contentNode.childElementCount ) - .map( ( contentNode ) => { - return contentNode.innerText; - } ) - ); - }, selector ); -}; - -const expectActiveButtonLabelToBe = async ( expected ) => { - await clickBlockToolbarButton( 'Align' ); - const activeButtonLabels = await getAlignmentToolbarLabels( { - getActiveButtonLabels: true, - } ); - expect( activeButtonLabels ).toHaveLength( 1 ); - expect( activeButtonLabels[ 0 ] ).toEqual( expected ); -}; - -const createShowsTheExpectedButtonsTest = ( blockName, buttonLabels ) => { - it( 'Shows the expected buttons on the alignment toolbar', async () => { - await insertBlock( blockName ); - await clickBlockToolbarButton( 'Align' ); - expect( await getAlignmentToolbarLabels() ).toEqual( - expect.arrayContaining( buttonLabels ) - ); - } ); -}; - -const createAppliesNoneAlignmentByDefaultTest = ( blockName ) => { - it( 'applies none alignment by default', async () => { - await insertBlock( blockName ); - await expectActiveButtonLabelToBe( alignLabels.none ); - } ); -}; - -const verifyMarkupIsValid = async ( htmlMarkup ) => { - await setPostContent( htmlMarkup ); - const blocks = await getAllBlocks(); - expect( blocks ).toHaveLength( 1 ); - expect( blocks[ 0 ].isValid ).toBeTruthy(); -}; - -const createCorrectlyAppliesAndRemovesAlignmentTest = ( - blockName, - alignment -) => { - it( 'Correctly applies the selected alignment and correctly removes the alignment', async () => { - const BUTTON_XPATH = `//button[contains(@class,'components-dropdown-menu__menu-item')]//span[contains(text(), '${ alignLabels[ alignment ] }')]`; - - // Set the specified alignment. - await insertBlock( blockName ); - await clickBlockToolbarButton( 'Align' ); - await ( await page.$x( BUTTON_XPATH ) )[ 0 ].click(); - - // Verify the button of the specified alignment is pressed. - await expectActiveButtonLabelToBe( alignLabels[ alignment ] ); - - let htmlMarkup = await getEditedPostContent(); - - // Verify the markup of the selected alignment was generated. - expect( htmlMarkup ).toMatchSnapshot(); - - // Verify the markup can be correctly parsed. - await verifyMarkupIsValid( htmlMarkup ); - - await selectBlockByClientId( ( await getAllBlocks() )[ 0 ].clientId ); - - // Remove the alignment. - await clickBlockToolbarButton( 'Align' ); - await ( await page.$x( BUTTON_XPATH ) )[ 0 ].click(); - - // Verify 'none' alignment button is in pressed state. - await expectActiveButtonLabelToBe( alignLabels.none ); - - // Verify alignment markup was removed from the block. - htmlMarkup = await getEditedPostContent(); - expect( htmlMarkup ).toMatchSnapshot(); - - // verify the markup when no alignment is set is valid - await verifyMarkupIsValid( htmlMarkup ); - - await selectBlockByClientId( ( await getAllBlocks() )[ 0 ].clientId ); - - // Verify alignment `none` button is in pressed state after parsing the block. - await expectActiveButtonLabelToBe( alignLabels.none ); - } ); -}; - -describe( 'Align Hook Works As Expected', () => { - beforeAll( async () => { - await activatePlugin( 'gutenberg-test-align-hook' ); - } ); - - beforeEach( async () => { - await createNewPost(); - } ); - - afterAll( async () => { - await deactivatePlugin( 'gutenberg-test-align-hook' ); - } ); - - describe( 'Block with no alignment set', () => { - const BLOCK_NAME = 'Test No Alignment Set'; - it( 'Shows no alignment buttons on the alignment toolbar', async () => { - await insertBlock( BLOCK_NAME ); - const CHANGE_ALIGNMENT_BUTTON_SELECTOR = - '.block-editor-block-toolbar .components-dropdown-menu__toggle[aria-label="Align"]'; - const changeAlignmentButton = await page.$( - CHANGE_ALIGNMENT_BUTTON_SELECTOR - ); - expect( changeAlignmentButton ).toBe( null ); - } ); - - it( 'Does not save any alignment related attribute or class', async () => { - await insertBlock( BLOCK_NAME ); - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - } ); - - describe( 'Block with align true', () => { - const BLOCK_NAME = 'Test Align True'; - - createShowsTheExpectedButtonsTest( - BLOCK_NAME, - Object.values( alignLabels ) - ); - - createAppliesNoneAlignmentByDefaultTest( BLOCK_NAME ); - - createCorrectlyAppliesAndRemovesAlignmentTest( BLOCK_NAME, 'right' ); - } ); - - describe( 'Block with align array', () => { - const BLOCK_NAME = 'Test Align Array'; - - createShowsTheExpectedButtonsTest( BLOCK_NAME, [ - alignLabels.none, - alignLabels.left, - alignLabels.center, - ] ); - - createAppliesNoneAlignmentByDefaultTest( BLOCK_NAME ); - - createCorrectlyAppliesAndRemovesAlignmentTest( BLOCK_NAME, 'center' ); - } ); - - describe( 'Block with default align', () => { - const BLOCK_NAME = 'Test Default Align'; - const SELECTED_ALIGNMENT_CONTROL_SELECTOR = - '//div[contains(@class, "components-dropdown-menu__menu")]//button[contains(@class, "is-active")]/span[text()="Align right"]'; - createShowsTheExpectedButtonsTest( - BLOCK_NAME, - Object.values( alignLabels ) - ); - - it( 'Applies the selected alignment by default', async () => { - await insertBlock( BLOCK_NAME ); - // Verify the correct alignment button is pressed. - await clickBlockToolbarButton( 'Align' ); - const selectedAlignmentControls = await page.$x( - SELECTED_ALIGNMENT_CONTROL_SELECTOR - ); - expect( selectedAlignmentControls ).toHaveLength( 1 ); - } ); - - it( 'The default markup does not contain the alignment attribute but contains the alignment class', async () => { - await insertBlock( BLOCK_NAME ); - const markup = await getEditedPostContent(); - expect( markup ).not.toContain( '"align":"right"' ); - expect( markup ).toContain( 'alignright' ); - } ); - - it( 'Can remove the default alignment and the align attribute equals none but alignnone class is not applied', async () => { - await insertBlock( BLOCK_NAME ); - // Remove the alignment. - await clickBlockToolbarButton( 'Align' ); - const [ selectedAlignmentControl ] = await page.$x( - SELECTED_ALIGNMENT_CONTROL_SELECTOR - ); - await selectedAlignmentControl.click(); - const markup = await getEditedPostContent(); - expect( markup ).toContain( '"align":""' ); - } ); - - createCorrectlyAppliesAndRemovesAlignmentTest( BLOCK_NAME, 'center' ); - } ); -} ); 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/plugins/innerblocks-locking-all-embed.js b/packages/e2e-tests/specs/editor/plugins/innerblocks-locking-all-embed.js deleted file mode 100644 index a0a83fbf90e616..00000000000000 --- a/packages/e2e-tests/specs/editor/plugins/innerblocks-locking-all-embed.js +++ /dev/null @@ -1,56 +0,0 @@ -/** - * WordPress dependencies - */ -import { - activatePlugin, - createNewPost, - deactivatePlugin, - insertBlock, - createEmbeddingMatcher, - createJSONResponse, - setUpResponseMocking, -} from '@wordpress/e2e-test-utils'; - -const MOCK_RESPONSES = [ - { - match: createEmbeddingMatcher( 'https://twitter.com/wordpress' ), - onRequestMatch: createJSONResponse( { - url: 'https://twitter.com/wordpress', - html: '

Mock success response.

', - type: 'rich', - provider_name: 'Twitter', - provider_url: 'https://twitter.com', - version: '1.0', - } ), - }, -]; - -describe( 'Embed block inside a locked all parent', () => { - beforeAll( async () => { - await activatePlugin( 'gutenberg-test-innerblocks-locking-all-embed' ); - } ); - - beforeEach( async () => { - await setUpResponseMocking( MOCK_RESPONSES ); - await createNewPost(); - } ); - - afterAll( async () => { - await deactivatePlugin( - 'gutenberg-test-innerblocks-locking-all-embed' - ); - } ); - - it( 'embed block should be able to embed external content', async () => { - await insertBlock( 'Test Inner Blocks Locking All Embed' ); - const embedInputSelector = - '.components-placeholder__input[aria-label="Embed URL"]'; - await page.waitForSelector( embedInputSelector ); - await page.click( embedInputSelector ); - // This URL should not have a trailing slash. - await page.keyboard.type( 'https://twitter.com/wordpress' ); - await page.keyboard.press( 'Enter' ); - // The twitter block should appear correctly. - await page.waitForSelector( 'figure.wp-block-embed' ); - } ); -} ); 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/e2e-tests/specs/site-editor/multi-entity-saving.test.js b/packages/e2e-tests/specs/site-editor/multi-entity-saving.test.js deleted file mode 100644 index fe5334d2a12769..00000000000000 --- a/packages/e2e-tests/specs/site-editor/multi-entity-saving.test.js +++ /dev/null @@ -1,239 +0,0 @@ -/** - * WordPress dependencies - */ -import { - createNewPost, - disablePrePublishChecks, - getOption, - insertBlock, - publishPost, - setOption, - trashAllPosts, - activateTheme, - clickButton, - createReusableBlock, - deleteAllTemplates, - canvas, -} from '@wordpress/e2e-test-utils'; - -describe( 'Multi-entity save flow', () => { - // Selectors - usable between Post/Site editors. - const checkedBoxSelector = '.components-checkbox-control__checked'; - const checkboxInputSelector = '.components-checkbox-control__input'; - const entitiesSaveSelector = '.editor-entities-saved-states__save-button'; - const savePanelSelector = '.entities-saved-states__panel'; - const closePanelButtonSelector = - '.editor-post-publish-panel__header-cancel-button button:not(:disabled)'; - - // Reusable assertions across Post/Site editors. - const assertAllBoxesChecked = async () => { - const checkedBoxes = await page.$$( checkedBoxSelector ); - const checkboxInputs = await page.$$( checkboxInputSelector ); - expect( checkedBoxes.length - checkboxInputs.length ).toBe( 0 ); - }; - const assertExistence = async ( selector, shouldBePresent ) => { - const element = await page.$( selector ); - if ( shouldBePresent ) { - expect( element ).not.toBeNull(); - } else { - expect( element ).toBeNull(); - } - }; - - let originalSiteTitle, originalBlogDescription; - - beforeAll( async () => { - await activateTheme( 'emptytheme' ); - await deleteAllTemplates( 'wp_template' ); - await deleteAllTemplates( 'wp_template_part' ); - await trashAllPosts( 'wp_block' ); - - // Get the current Site Title and Site Tagline, so that we can reset - // them back to the original values once the test suite has finished. - originalSiteTitle = await getOption( 'blogname' ); - originalBlogDescription = await getOption( 'blogdescription' ); - } ); - - afterAll( async () => { - await activateTheme( 'twentytwentyone' ); - - // Reset the Site Title and Site Tagline back to their original values. - await setOption( 'blogname', originalSiteTitle ); - await setOption( 'blogdescription', originalBlogDescription ); - } ); - - describe( 'Post Editor', () => { - // Selectors - Post editor specific. - const draftSavedSelector = '.editor-post-saved-state.is-saved'; - const multiSaveSelector = - '.editor-post-publish-button__button.has-changes-dot'; - const savePostSelector = '.editor-post-publish-button__button'; - const enabledSavePostSelector = `${ savePostSelector }[aria-disabled=false]`; - const publishA11ySelector = - '.edit-post-layout__toggle-publish-panel-button'; - const saveA11ySelector = - '.edit-post-layout__toggle-entities-saved-states-panel-button'; - const publishPanelSelector = '.editor-post-publish-panel'; - - // Reusable assertions inside Post editor. - const assertMultiSaveEnabled = async () => { - const multiSaveButton = - await page.waitForSelector( multiSaveSelector ); - expect( multiSaveButton ).not.toBeNull(); - }; - const assertMultiSaveDisabled = async () => { - const multiSaveButton = await page.waitForSelector( - multiSaveSelector, - { hidden: true } - ); - expect( multiSaveButton ).toBeNull(); - }; - - it( 'Save flow should work as expected.', async () => { - await createNewPost(); - // Edit the page some. - await canvas().waitForSelector( '.editor-post-title' ); - await canvas().click( '.editor-post-title' ); - await page.keyboard.type( 'Test Post...' ); - await page.keyboard.press( 'Enter' ); - - // Should not trigger multi-entity save button with only post edited. - await assertMultiSaveDisabled(); - - // Should only have publish panel a11y button active with only post edited. - await assertExistence( publishA11ySelector, true ); - await assertExistence( saveA11ySelector, false ); - await assertExistence( publishPanelSelector, false ); - await assertExistence( savePanelSelector, false ); - - // Add a reusable block and edit it. - await createReusableBlock( 'Hi!', 'Test' ); - await canvas().waitForSelector( 'p[data-type="core/paragraph"]' ); - await canvas().click( 'p[data-type="core/paragraph"]' ); - await page.keyboard.type( 'Oh!' ); - - // Should trigger multi-entity save button once template part edited. - await assertMultiSaveEnabled(); - - // Should only have save panel a11y button active after child entities edited. - await assertExistence( publishA11ySelector, false ); - await assertExistence( saveA11ySelector, true ); - await assertExistence( publishPanelSelector, false ); - await assertExistence( savePanelSelector, false ); - - // Opening panel has boxes checked by default. - await page.click( savePostSelector ); - await page.waitForSelector( savePanelSelector ); - await assertAllBoxesChecked(); - - // Should not show other panels (or their a11y buttons) while save panel opened. - await assertExistence( publishA11ySelector, false ); - await assertExistence( saveA11ySelector, false ); - await assertExistence( publishPanelSelector, false ); - - // Publish panel should open after saving. - await page.click( entitiesSaveSelector ); - await page.waitForSelector( publishPanelSelector ); - - // No other panels (or their a11y buttons) should be present with publish panel open. - await assertExistence( publishA11ySelector, false ); - await assertExistence( saveA11ySelector, false ); - await assertExistence( savePanelSelector, false ); - - // Close publish panel. - const closePanelButton = await page.waitForSelector( - closePanelButtonSelector - ); - await closePanelButton.click(); - - // Verify saving is disabled. - const draftSaved = await page.waitForSelector( draftSavedSelector ); - expect( draftSaved ).not.toBeNull(); - await assertMultiSaveDisabled(); - await assertExistence( saveA11ySelector, false ); - - await publishPost(); - // Wait for the success notice specifically for the published post. - // `publishPost()` has a similar check but it only checks for the - // existence of any snackbars. In this case, there's another "Site updated" - // notice which will be sufficient for that and thus creating a false-positive. - await page.waitForXPath( - '//*[@id="a11y-speak-polite"][contains(text(), "Post published")]' - ); - - // Unselect the blocks to avoid clicking the block toolbar. - await page.evaluate( () => { - wp.data.dispatch( 'core/block-editor' ).clearSelectedBlock(); - } ); - - // Update the post. - await canvas().click( '.editor-post-title' ); - await page.keyboard.type( '...more title!' ); - - // Verify update button is enabled. - const enabledSaveButton = await page.waitForSelector( - enabledSavePostSelector - ); - expect( enabledSaveButton ).not.toBeNull(); - // Verify multi-entity saving not enabled. - await assertMultiSaveDisabled(); - await assertExistence( saveA11ySelector, false ); - - // Update reusable block again. - await canvas().click( 'p[data-type="core/paragraph"]' ); - // We need to click again due to the clickthrough overlays in reusable blocks. - await canvas().click( 'p[data-type="core/paragraph"]' ); - await page.keyboard.type( 'R!' ); - - // Multi-entity saving should be enabled. - await assertMultiSaveEnabled(); - await assertExistence( saveA11ySelector, true ); - } ); - - it( 'Site blocks should save individually', async () => { - await createNewPost(); - await disablePrePublishChecks(); - - await insertBlock( 'Site Title' ); - // Ensure title is retrieved before typing. - await page.waitForXPath( - `//a[contains(text(), "${ originalSiteTitle }")]` - ); - const editableSiteTitleSelector = - '.wp-block-site-title a[contenteditable="true"]'; - await canvas().waitForSelector( editableSiteTitleSelector ); - await canvas().focus( editableSiteTitleSelector ); - await page.keyboard.type( '...' ); - - await insertBlock( 'Site Tagline' ); - // Wait for the placeholder. - await canvas().waitForXPath( - '//span[@data-rich-text-placeholder="Write site tagline…"]' - ); - const editableSiteTagLineSelector = - '.wp-block-site-tagline[contenteditable="true"]'; - await canvas().waitForSelector( editableSiteTagLineSelector ); - await canvas().focus( editableSiteTagLineSelector ); - await page.keyboard.type( 'Just another WordPress site' ); - - await clickButton( 'Publish' ); - await page.waitForSelector( savePanelSelector ); - let checkboxInputs = await page.$$( checkboxInputSelector ); - expect( checkboxInputs ).toHaveLength( 3 ); - - await checkboxInputs[ 1 ].click(); - await page.click( entitiesSaveSelector ); - - // Wait for the snackbar notice that the post has been published. - await page.waitForSelector( '.components-snackbar' ); - - await clickButton( 'Update…' ); - await page.waitForSelector( savePanelSelector ); - - await page.waitForSelector( checkboxInputSelector ); - checkboxInputs = await page.$$( checkboxInputSelector ); - - expect( checkboxInputs ).toHaveLength( 1 ); - } ); - } ); -} ); diff --git a/packages/e2e-tests/specs/site-editor/site-editor-export.test.js b/packages/e2e-tests/specs/site-editor/site-editor-export.test.js deleted file mode 100644 index 0e560e9b7e0ade..00000000000000 --- a/packages/e2e-tests/specs/site-editor/site-editor-export.test.js +++ /dev/null @@ -1,63 +0,0 @@ -/** - * External dependencies - */ -import fs from 'fs'; -import path from 'path'; -import os from 'os'; - -/** - * WordPress dependencies - */ -import { - deleteAllTemplates, - activateTheme, - visitSiteEditor, - enterEditMode, - clickOnMoreMenuItem, -} from '@wordpress/e2e-test-utils'; - -async function waitForFileExists( filePath, timeout = 10000 ) { - const start = Date.now(); - while ( ! fs.existsSync( filePath ) ) { - // Puppeteer doesn't have an API for managing file downloads. - // We are using `waitForTimeout` to add delays between check of file existence. - // eslint-disable-next-line no-restricted-syntax - await page.waitForTimeout( 1000 ); - if ( Date.now() - start > timeout ) { - throw Error( 'waitForFileExists timeout' ); - } - } -} - -describe( 'Site Editor Templates Export', () => { - beforeAll( async () => { - await activateTheme( 'emptytheme' ); - await deleteAllTemplates( 'wp_template' ); - await deleteAllTemplates( 'wp_template_part' ); - } ); - - afterAll( async () => { - await activateTheme( 'twentytwentyone' ); - } ); - - beforeEach( async () => { - await visitSiteEditor(); - await enterEditMode(); - } ); - - it( 'clicking export should download emptytheme.zip file', async () => { - const directory = fs.mkdtempSync( - path.join( os.tmpdir(), 'test-edit-site-export-' ) - ); - await page._client.send( 'Page.setDownloadBehavior', { - behavior: 'allow', - downloadPath: directory, - } ); - - await clickOnMoreMenuItem( 'Export', 'site-editor' ); - const filePath = path.join( directory, 'emptytheme.zip' ); - await waitForFileExists( filePath ); - expect( fs.existsSync( filePath ) ).toBe( true ); - fs.unlinkSync( filePath ); - } ); -} ); 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..fc03c32008ee06 100644 --- a/packages/edit-site/src/components/dataviews/dataviews.js +++ b/packages/edit-site/src/components/dataviews/dataviews.js @@ -10,13 +10,13 @@ import { useMemo } from '@wordpress/element'; /** * Internal dependencies */ -import ViewList from './view-list'; import Pagination from './pagination'; import ViewActions from './view-actions'; import Filters from './filters'; import Search from './search'; -import { ViewGrid } from './view-grid'; -import { ViewSideBySide } from './view-side-by-side'; +import ViewList from './view-list'; +import ViewGrid from './view-grid'; +import ViewSideBySide from './view-side-by-side'; // To do: convert to view type registry. export const viewTypeSupportsMap = { @@ -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 76e6fca78de67d..68a00b81bb63f6 100644 --- a/packages/edit-site/src/components/dataviews/style.scss +++ b/packages/edit-site/src/components/dataviews/style.scss @@ -1,8 +1,8 @@ .dataviews-wrapper { width: 100%; - height: 100%; + height: calc(100% - #{$grid-unit-40} * 2); overflow: auto; - padding: $grid-unit-40; + padding: $grid-unit-40 $grid-unit-40 0; > div { min-height: 100%; @@ -11,6 +11,15 @@ .dataviews-pagination { margin-top: auto; + position: sticky; + bottom: 0; + background-color: $white; + padding: $grid-unit-20 0; + border-top: $border-width solid $gray-200; +} + +.dataviews-filters-options { + margin: $grid-unit-40 0 $grid-unit-20; } .dataviews-list-view { @@ -19,14 +28,15 @@ border-color: inherit; border-collapse: collapse; position: relative; + a { text-decoration: none; } 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 { @@ -37,16 +47,82 @@ } tr { border-bottom: 1px solid $gray-100; + + &:last-child { + border-bottom: 0; + } + } + thead { + tr { + border: 0; + } + th { + position: sticky; + top: - #{$grid-unit-40}; // Offset the container padding + background-color: $white; + box-shadow: inset 0 -#{$border-width} 0 $gray-200; + } } } -.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 + } - > * { - max-width: 100%; - object-fit: cover; + .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; + + > * { + 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..70248eb72d5030 100644 --- a/packages/edit-site/src/components/dataviews/view-actions.js +++ b/packages/edit-site/src/components/dataviews/view-actions.js @@ -9,10 +9,10 @@ import { import { chevronRightSmall, check, - blockTable, + formatListBullets, arrowUp, arrowDown, - grid, + category, columns, } from '@wordpress/icons'; import { __ } from '@wordpress/i18n'; @@ -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; @@ -275,7 +276,11 @@ function SortMenu( { fields, view, onChangeView } ) { ); } -const VIEW_TYPE_ICONS = { list: blockTable, grid, 'side-by-side': columns }; +const VIEW_TYPE_ICONS = { + list: formatListBullets, + grid: category, + 'side-by-side': columns, +}; export default function ViewActions( { fields, diff --git a/packages/edit-site/src/components/dataviews/view-grid.js b/packages/edit-site/src/components/dataviews/view-grid.js index fd74d4f401a966..8a39bdd8353d1a 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'; @@ -15,14 +13,19 @@ import { useAsyncList } from '@wordpress/compose'; */ import ItemActions from './item-actions'; -export function ViewGrid( { data, fields, view, actions, getItemId } ) { +export default 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/dataviews/view-side-by-side.js b/packages/edit-site/src/components/dataviews/view-side-by-side.js index 47b1551b379b31..9b06ca799096eb 100644 --- a/packages/edit-site/src/components/dataviews/view-side-by-side.js +++ b/packages/edit-site/src/components/dataviews/view-side-by-side.js @@ -3,7 +3,7 @@ */ import ViewList from './view-list'; -export function ViewSideBySide( props ) { +export default function ViewSideBySide( props ) { // To do: change to email-like preview list. return ; } diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/utils/get-intersecting-font-faces.js b/packages/edit-site/src/components/global-styles/font-library-modal/utils/get-intersecting-font-faces.js index a3ffd31db2288d..e21e72c58ed533 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/utils/get-intersecting-font-faces.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/utils/get-intersecting-font-faces.js @@ -47,7 +47,7 @@ export default function getIntersectingFontFaces( incoming, existing ) { } ); } ); - matches.push( { ...existingFont, fontFace: matchingFaces } ); + matches.push( { ...incomingFont, fontFace: matchingFaces } ); } else { matches.push( incomingFont ); } diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/utils/index.js b/packages/edit-site/src/components/global-styles/font-library-modal/utils/index.js index d0a57978bcce94..f5723f5814e983 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/utils/index.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/utils/index.js @@ -1,8 +1,12 @@ +/** + * External dependencies + */ +import { paramCase as kebabCase } from 'change-case'; + /** * Internal dependencies */ import { FONT_WEIGHTS, FONT_STYLES } from './constants'; -import { formatFontFamily } from './preview-styles'; export function setUIValuesNeeded( font, extraValues = {} ) { if ( ! font.name && ( font.fontFamily || font.slug ) ) { @@ -85,14 +89,10 @@ export async function loadFontFaceInBrowser( fontFace, source, addTo = 'all' ) { } // eslint-disable-next-line no-undef - const newFont = new FontFace( - formatFontFamily( fontFace.fontFamily ), - dataSource, - { - style: fontFace.fontStyle, - weight: fontFace.fontWeight, - } - ); + const newFont = new FontFace( fontFace.fontFamily, dataSource, { + style: fontFace.fontStyle, + weight: fontFace.fontWeight, + } ); const loadedFace = await newFont.load(); @@ -129,9 +129,20 @@ export function getDisplaySrcFromFontFace( input, urlPrefix ) { return src; } +// This function replicates one behavior of _wp_to_kebab_case(). +// Additional context: https://github.com/WordPress/gutenberg/issues/53695 +export function wpKebabCase( str ) { + // If a string contains a digit followed by a number, insert a dash between them. + return kebabCase( str ).replace( + /([a-zA-Z])(\d)|(\d)([a-zA-Z])/g, + '$1$3-$2$4' + ); +} + export function makeFormDataFromFontFamilies( fontFamilies ) { const formData = new FormData(); const newFontFamilies = fontFamilies.map( ( family, familyIndex ) => { + family.slug = wpKebabCase( family.slug ); if ( family?.fontFace ) { family.fontFace = family.fontFace.map( ( face, faceIndex ) => { if ( face.file ) { diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/getIntersectingFontFaces.spec.js b/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/getIntersectingFontFaces.spec.js index 91ae5f45d66da6..9899005ad65b89 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/getIntersectingFontFaces.spec.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/getIntersectingFontFaces.spec.js @@ -5,7 +5,7 @@ import getIntersectingFontFaces from '../get-intersecting-font-faces'; describe( 'getIntersectingFontFaces', () => { it( 'returns matching font faces for matching font family', () => { - const intendedFontsFamilies = [ + const incomingFontFamilies = [ { slug: 'lobster', fontFace: [ @@ -30,15 +30,15 @@ describe( 'getIntersectingFontFaces', () => { ]; const result = getIntersectingFontFaces( - intendedFontsFamilies, + incomingFontFamilies, existingFontFamilies ); - expect( result ).toEqual( intendedFontsFamilies ); + expect( result ).toEqual( incomingFontFamilies ); } ); it( 'returns empty array when there is no match', () => { - const intendedFontsFamilies = [ + const incomingFontFamilies = [ { slug: 'lobster', fontFace: [ @@ -63,7 +63,7 @@ describe( 'getIntersectingFontFaces', () => { ]; const result = getIntersectingFontFaces( - intendedFontsFamilies, + incomingFontFamilies, existingFontFamilies ); @@ -71,7 +71,7 @@ describe( 'getIntersectingFontFaces', () => { } ); it( 'returns matching font faces', () => { - const intendedFontsFamilies = [ + const incomingFontFamilies = [ { slug: 'lobster', fontFace: [ @@ -129,7 +129,7 @@ describe( 'getIntersectingFontFaces', () => { ]; const result = getIntersectingFontFaces( - intendedFontsFamilies, + incomingFontFamilies, existingFontFamilies ); @@ -137,7 +137,7 @@ describe( 'getIntersectingFontFaces', () => { } ); it( 'returns empty array when the first list is empty', () => { - const intendedFontsFamilies = []; + const incomingFontFamilies = []; const existingFontFamilies = [ { @@ -152,7 +152,7 @@ describe( 'getIntersectingFontFaces', () => { ]; const result = getIntersectingFontFaces( - intendedFontsFamilies, + incomingFontFamilies, existingFontFamilies ); @@ -160,7 +160,7 @@ describe( 'getIntersectingFontFaces', () => { } ); it( 'returns empty array when the second list is empty', () => { - const intendedFontsFamilies = [ + const incomingFontFamilies = [ { slug: 'lobster', fontFace: [ @@ -175,7 +175,7 @@ describe( 'getIntersectingFontFaces', () => { const existingFontFamilies = []; const result = getIntersectingFontFaces( - intendedFontsFamilies, + incomingFontFamilies, existingFontFamilies ); @@ -183,7 +183,7 @@ describe( 'getIntersectingFontFaces', () => { } ); it( 'returns intersecting font family when there are no fonfaces', () => { - const intendedFontsFamilies = [ + const incomingFontFamilies = [ { slug: 'piazzolla', fontFace: [ { fontStyle: 'normal', fontWeight: '400' } ], @@ -200,7 +200,7 @@ describe( 'getIntersectingFontFaces', () => { ]; const result = getIntersectingFontFaces( - intendedFontsFamilies, + incomingFontFamilies, existingFontFamilies ); @@ -208,7 +208,7 @@ describe( 'getIntersectingFontFaces', () => { } ); it( 'returns intersecting if there is an intended font face and is not present in the returning it should not be returned', () => { - const intendedFontsFamilies = [ + const incomingFontFamilies = [ { slug: 'piazzolla', fontFace: [ { fontStyle: 'normal', fontWeight: '400' } ], @@ -226,7 +226,7 @@ describe( 'getIntersectingFontFaces', () => { ]; const result = getIntersectingFontFaces( - intendedFontsFamilies, + incomingFontFamilies, existingFontFamilies ); const expected = [ @@ -237,4 +237,35 @@ describe( 'getIntersectingFontFaces', () => { ]; expect( result ).toEqual( expected ); } ); + + it( 'updates font family definition using the incoming data', () => { + const incomingFontFamilies = [ + { + slug: 'gothic-a1', + fontFace: [ { fontStyle: 'normal', fontWeight: '400' } ], + fontFamily: "'Gothic A1', serif", + }, + ]; + + const existingFontFamilies = [ + { + slug: 'gothic-a1', + fontFace: [ { fontStyle: 'normal', fontWeight: '400' } ], + fontFamily: 'Gothic A1, serif', + }, + ]; + + const result = getIntersectingFontFaces( + incomingFontFamilies, + existingFontFamilies + ); + const expected = [ + { + slug: 'gothic-a1', + fontFace: [ { fontStyle: 'normal', fontWeight: '400' } ], + fontFamily: "'Gothic A1', serif", + }, + ]; + expect( result ).toEqual( expected ); + } ); } ); diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/wpKebabCase.spec.js b/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/wpKebabCase.spec.js new file mode 100644 index 00000000000000..d296117ff3a49b --- /dev/null +++ b/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/wpKebabCase.spec.js @@ -0,0 +1,28 @@ +/** + * Internal dependencies + */ +import { wpKebabCase } from '../index'; + +describe( 'wpKebabCase', () => { + it( 'should insert a dash between a letter and a digit', () => { + const input = 'abc1def'; + const expectedOutput = 'abc-1def'; + expect( wpKebabCase( input ) ).toEqual( expectedOutput ); + + const input2 = 'abc1def2ghi'; + const expectedOutput2 = 'abc-1def-2ghi'; + expect( wpKebabCase( input2 ) ).toEqual( expectedOutput2 ); + } ); + + it( 'should not insert a dash between two letters', () => { + const input = 'abcdef'; + const expectedOutput = 'abcdef'; + expect( wpKebabCase( input ) ).toEqual( expectedOutput ); + } ); + + it( 'should not insert a dash between a digit and a hyphen', () => { + const input = 'abc1-def'; + const expectedOutput = 'abc-1-def'; + expect( wpKebabCase( input ) ).toEqual( expectedOutput ); + } ); +} ); diff --git a/packages/edit-site/src/components/global-styles/header.js b/packages/edit-site/src/components/global-styles/header.js index f62820653ff925..e6da4115217f57 100644 --- a/packages/edit-site/src/components/global-styles/header.js +++ b/packages/edit-site/src/components/global-styles/header.js @@ -12,7 +12,7 @@ import { import { isRTL, __ } from '@wordpress/i18n'; import { chevronRight, chevronLeft } from '@wordpress/icons'; -function ScreenHeader( { title, description } ) { +function ScreenHeader( { title, description, onBack } ) { return ( @@ -27,6 +27,7 @@ function ScreenHeader( { title, description } ) { icon={ isRTL() ? chevronRight : chevronLeft } isSmall aria-label={ __( 'Navigate to the previous view' ) } + onClick={ onBack } /> { - return { - editorCanvasContainerView: unlock( - select( editSiteStore ) - ).getEditorCanvasContainerView(), - blocks: select( blockEditorStore ).getBlocks(), - }; - }, [] ); + const { blocks, editorCanvasContainerView, revisionsCount } = useSelect( + ( select ) => { + const { + getEntityRecord, + __experimentalGetCurrentGlobalStylesId, + __experimentalGetDirtyEntityRecords, + } = select( coreStore ); + const isDirty = __experimentalGetDirtyEntityRecords().length > 0; + const globalStylesId = __experimentalGetCurrentGlobalStylesId(); + const globalStyles = globalStylesId + ? getEntityRecord( 'root', 'globalStyles', globalStylesId ) + : undefined; + let _revisionsCount = + globalStyles?._links?.[ 'version-history' ]?.[ 0 ]?.count || 0; + // one for the reset item. + _revisionsCount++; + // one for any dirty changes (unsaved). + if ( isDirty ) { + _revisionsCount++; + } + return { + editorCanvasContainerView: unlock( + select( editSiteStore ) + ).getEditorCanvasContainerView(), + blocks: select( blockEditorStore ).getBlocks(), + revisionsCount: _revisionsCount, + }; + }, + [] + ); const { revisions, isLoading, hasUnsavedChanges } = useGlobalStylesRevisions(); const [ currentlySelectedRevision, setCurrentlySelectedRevision ] = @@ -61,6 +84,7 @@ function ScreenRevisions() { const onCloseRevisions = () => { goTo( '/' ); // Return to global styles main panel. + setEditorCanvasContainerView( undefined ); }; const restoreRevision = ( revision ) => { @@ -119,10 +143,15 @@ function ScreenRevisions() { return ( <> { isLoading && ( diff --git a/packages/edit-site/src/components/global-styles/style.scss b/packages/edit-site/src/components/global-styles/style.scss index 3560ef139fa3fe..a899495cc332b7 100644 --- a/packages/edit-site/src/components/global-styles/style.scss +++ b/packages/edit-site/src/components/global-styles/style.scss @@ -191,14 +191,3 @@ .edit-site-global-styles-sidebar__panel .block-editor-block-icon svg { fill: currentColor; } - -[class][class].edit-site-global-styles-sidebar__revisions-count-badge { - align-items: center; - background: $gray-800; - border-radius: 2px; - color: $white; - display: inline-flex; - justify-content: center; - min-height: $icon-size; - min-width: $icon-size; -} diff --git a/packages/edit-site/src/components/global-styles/ui.js b/packages/edit-site/src/components/global-styles/ui.js index 2e33d4b599b7b9..c8d72205c3bed8 100644 --- a/packages/edit-site/src/components/global-styles/ui.js +++ b/packages/edit-site/src/components/global-styles/ui.js @@ -1,8 +1,3 @@ -/** - * External dependencies - */ -import classnames from 'classnames'; - /** * WordPress dependencies */ @@ -55,6 +50,7 @@ const { Slot: GlobalStylesMenuSlot, Fill: GlobalStylesMenuFill } = createSlotFill( SLOT_FILL_NAME ); function GlobalStylesActionMenu() { + const [ canReset, onReset ] = useGlobalStylesReset(); const { toggle } = useDispatch( preferencesStore ); const { canEditCSS } = useSelect( ( select ) => { const { getEntityRecord, __experimentalGetCurrentGlobalStylesId } = @@ -69,49 +65,56 @@ function GlobalStylesActionMenu() { canEditCSS: !! globalStyles?._links?.[ 'wp:action-edit-css' ], }; }, [] ); + const { setEditorCanvasContainerView } = unlock( + useDispatch( editSiteStore ) + ); const { goTo } = useNavigator(); - const loadCustomCSS = () => goTo( '/css' ); + const loadCustomCSS = () => { + setEditorCanvasContainerView( 'global-styles-css' ); + goTo( '/css' ); + }; return ( { ( { onClose } ) => ( - - { canEditCSS && ( - - { __( 'Additional CSS' ) } + <> + + { canEditCSS && ( + + { __( 'Additional CSS' ) } + + ) } + { + toggle( + 'core/edit-site', + 'welcomeGuideStyles' + ); + onClose(); + } } + > + { __( 'Welcome Guide' ) } + + + + { + onReset(); + onClose(); + } } + disabled={ ! canReset } + > + { __( 'Reset styles' ) } - ) } - { - toggle( - 'core/edit-site', - 'welcomeGuideStyles' - ); - onClose(); - } } - > - { __( 'Welcome Guide' ) } - - + + ) } ); } -function RevisionsCountBadge( { className, children } ) { - return ( - - { children } - - ); -} function GlobalStylesRevisionsMenu() { const { setIsListViewOpened } = useDispatch( editSiteStore ); const { revisionsCount } = useSelect( ( select ) => { @@ -128,56 +131,38 @@ function GlobalStylesRevisionsMenu() { globalStyles?._links?.[ 'version-history' ]?.[ 0 ]?.count ?? 0, }; }, [] ); - const [ canReset, onReset ] = useGlobalStylesReset(); const { goTo } = useNavigator(); const { setEditorCanvasContainerView } = unlock( useDispatch( editSiteStore ) ); + const isRevisionsOpened = useSelect( + ( select ) => + 'global-styles-revisions' === + unlock( select( editSiteStore ) ).getEditorCanvasContainerView(), + [] + ); const loadRevisions = () => { setIsListViewOpened( false ); - goTo( '/revisions' ); - setEditorCanvasContainerView( 'global-styles-revisions' ); + + if ( ! isRevisionsOpened ) { + goTo( '/revisions' ); + setEditorCanvasContainerView( 'global-styles-revisions' ); + } else { + goTo( '/' ); + setEditorCanvasContainerView( undefined ); + } }; const hasRevisions = revisionsCount > 0; return ( - { canReset || hasRevisions ? ( - - { ( { onClose } ) => ( - - { hasRevisions && ( - - { revisionsCount } - - } - > - { __( 'Revision history' ) } - - ) } - { - onReset(); - onClose(); - } } - disabled={ ! canReset } - > - { __( 'Reset to defaults' ) } - - - ) } - - ) : ( - - *
- * ``` - * - * @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-aztec/package.json b/packages/react-native-aztec/package.json index 631781600d78d8..e814be90943a74 100644 --- a/packages/react-native-aztec/package.json +++ b/packages/react-native-aztec/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/react-native-aztec", - "version": "1.109.0", + "version": "1.109.1", "description": "Aztec view for react-native.", "private": true, "author": "The WordPress Contributors", diff --git a/packages/react-native-bridge/package.json b/packages/react-native-bridge/package.json index 20ae851c89686b..ca7cfc3de79bcf 100644 --- a/packages/react-native-bridge/package.json +++ b/packages/react-native-bridge/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/react-native-bridge", - "version": "1.109.0", + "version": "1.109.1", "description": "Native bridge library used to integrate the block editor into a native App.", "private": true, "author": "The WordPress Contributors", diff --git a/packages/react-native-editor/CHANGELOG.md b/packages/react-native-editor/CHANGELOG.md index 635937c4d8ce0b..70a1aa4ccfc5dc 100644 --- a/packages/react-native-editor/CHANGELOG.md +++ b/packages/react-native-editor/CHANGELOG.md @@ -10,6 +10,12 @@ 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] +- [**] Editor displays network status when offline [#56627] +- [**] Fixes a crash on pasting MS Word list markup [#56653] + +## 1.109.1 +- [***] 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/react-native-editor/ios/Podfile.lock b/packages/react-native-editor/ios/Podfile.lock index d6f0ca39a09bf2..dd3021ab8a6dba 100644 --- a/packages/react-native-editor/ios/Podfile.lock +++ b/packages/react-native-editor/ios/Podfile.lock @@ -13,7 +13,7 @@ PODS: - ReactCommon/turbomodule/core (= 0.71.11) - fmt (6.2.1) - glog (0.3.5) - - Gutenberg (1.109.0): + - Gutenberg (1.109.1): - React-Core (= 0.71.11) - React-CoreModules (= 0.71.11) - React-RCTImage (= 0.71.11) @@ -429,7 +429,7 @@ PODS: - React-RCTImage - RNSVG (13.9.0): - React-Core - - RNTAztecView (1.109.0): + - RNTAztecView (1.109.1): - React-Core - WordPress-Aztec-iOS (= 1.19.9) - SDWebImage (5.11.1): @@ -617,7 +617,7 @@ SPEC CHECKSUMS: FBReactNativeSpec: f07662560742d82a5b73cee116c70b0b49bcc220 fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9 glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b - Gutenberg: dd556a8be3f8b5225862823f050e57d0a22e0614 + Gutenberg: ce2b737d183d0179cb86596412bad21d48eafdcb hermes-engine: 34c863b446d0135b85a6536fa5fd89f48196f848 libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 libwebp: 60305b2e989864154bd9be3d772730f08fc6a59c @@ -662,7 +662,7 @@ SPEC CHECKSUMS: RNReanimated: d4f363f4987ae0ade3e36ff81c94e68261bf4b8d RNScreens: 68fd1060f57dd1023880bf4c05d74784b5392789 RNSVG: 53c661b76829783cdaf9b7a57258f3d3b4c28315 - RNTAztecView: 8415d8e322e98d087b3f8fbba0669e84d6b235cb + RNTAztecView: 8d9b3bd517873101ab1ea89948b45c601bcedea0 SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d WordPress-Aztec-iOS: fbebd569c61baa252b3f5058c0a2a9a6ada686bb diff --git a/packages/react-native-editor/package.json b/packages/react-native-editor/package.json index b599c2cc51c65a..aa41fc9ffa1af1 100644 --- a/packages/react-native-editor/package.json +++ b/packages/react-native-editor/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/react-native-editor", - "version": "1.109.0", + "version": "1.109.1", "description": "Mobile WordPress gutenberg editor.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/react-native-editor/src/jsdom-patches.js b/packages/react-native-editor/src/jsdom-patches.js index f33dd892f8c18c..c86e4c82f31270 100644 --- a/packages/react-native-editor/src/jsdom-patches.js +++ b/packages/react-native-editor/src/jsdom-patches.js @@ -171,6 +171,18 @@ Element.prototype.closest = function ( selector ) { return null; }; +/** + * Implementation of Element.prototype.remove based on polyfills: + * - https://github.com/chenzhenxi/element-remove/blob/master/index.js + * (referenced in https://developer.mozilla.org/en-US/docs/Web/API/Element/remove#see_also) + * - https://github.com/JakeChampion/polyfill-library/blob/master/polyfills/Element/prototype/remove/polyfill.js + */ +Element.prototype.remove = function () { + if ( this.parentNode ) { + this.parentNode.removeChild( this ); + } +}; + /** * Helper function to check if a node implements the NonDocumentTypeChildNode * interface 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/component/index.js b/packages/rich-text/src/component/index.js index 87e57e49e4333e..0e9291b7a5e03d 100644 --- a/packages/rich-text/src/component/index.js +++ b/packages/rich-text/src/component/index.js @@ -71,7 +71,9 @@ export function useRichText( { function setRecordFromProps() { _value.current = value; record.current = create( { - html: preserveWhiteSpace ? value : collapseWhiteSpace( value ), + html: preserveWhiteSpace + ? value + : collapseWhiteSpace( typeof value === 'string' ? value : '' ), } ); if ( disableFormats ) { record.current.formats = Array( value.length ); 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/scripts/scripts/test-playwright.js b/packages/scripts/scripts/test-playwright.js index 71bc6a63320cf1..4a8b0762336abd 100644 --- a/packages/scripts/scripts/test-playwright.js +++ b/packages/scripts/scripts/test-playwright.js @@ -24,21 +24,28 @@ const { hasProjectFile, hasArgInCLI, getArgsFromCLI, + getAsBooleanFromENV, } = require( '../utils' ); -const result = spawn( - 'node', - [ - path.resolve( require.resolve( 'playwright-core' ), '..', 'cli.js' ), - 'install', - ], - { - stdio: 'inherit', - } -); +if ( ! getAsBooleanFromENV( 'PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD' ) ) { + const result = spawn( + 'node', + [ + path.resolve( + require.resolve( 'playwright-core' ), + '..', + 'cli.js' + ), + 'install', + ], + { + stdio: 'inherit', + } + ); -if ( result.status > 0 ) { - process.exit( result.status ); + if ( result.status > 0 ) { + process.exit( result.status ); + } } const config = diff --git a/packages/scripts/utils/index.js b/packages/scripts/utils/index.js index 870c2423361b53..ae93160381df44 100644 --- a/packages/scripts/utils/index.js +++ b/packages/scripts/utils/index.js @@ -1,6 +1,7 @@ /** * Internal dependencies */ +const { getAsBooleanFromENV } = require( './process' ); const { getArgFromCLI, getArgsFromCLI, @@ -28,6 +29,7 @@ const { getPackageProp, hasPackageProp } = require( './package' ); module.exports = { fromProjectRoot, fromConfigRoot, + getAsBooleanFromENV, getArgFromCLI, getArgsFromCLI, getFileArgsFromCLI, diff --git a/packages/scripts/utils/process.js b/packages/scripts/utils/process.js index de07d36a8b59c4..48b884dc085bce 100644 --- a/packages/scripts/utils/process.js +++ b/packages/scripts/utils/process.js @@ -1,3 +1,8 @@ +const getAsBooleanFromENV = ( name ) => { + const value = process.env[ name ]; + return !! value && value !== 'false' && value !== '0'; +}; + const getArgsFromCLI = ( excludePrefixes ) => { const args = process.argv.slice( 2 ); if ( excludePrefixes ) { @@ -12,6 +17,7 @@ const getArgsFromCLI = ( excludePrefixes ) => { module.exports = { exit: process.exit, + getAsBooleanFromENV, getArgsFromCLI, getCurrentWorkingDirectory: process.cwd, }; 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/plugins/inner-blocks-locking-all-embed.spec.js b/test/e2e/specs/editor/plugins/inner-blocks-locking-all-embed.spec.js new file mode 100644 index 00000000000000..3bf0ff459cb7fe --- /dev/null +++ b/test/e2e/specs/editor/plugins/inner-blocks-locking-all-embed.spec.js @@ -0,0 +1,59 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +const EMBED_URLS = [ + '/oembed/1.0/proxy', + `rest_route=${ encodeURIComponent( '/oembed/1.0/proxy' ) }`, +]; +const MOCK_RESPONSES = { + url: 'https://twitter.com/wordpress', + html: '

Mock success response.

', + type: 'rich', + provider_name: 'Twitter', + provider_url: 'https://twitter.com', + version: '1.0', +}; + +test.describe( 'Embed block inside a locked all parent', () => { + test.beforeAll( async ( { requestUtils } ) => { + await requestUtils.activatePlugin( + 'gutenberg-test-innerblocks-locking-all-embed' + ); + } ); + + test.afterAll( async ( { requestUtils } ) => { + await requestUtils.deactivatePlugin( + 'gutenberg-test-innerblocks-locking-all-embed' + ); + } ); + + test( 'embed block should be able to embed external content', async ( { + admin, + editor, + page, + } ) => { + await admin.createNewPost(); + await page.route( + ( url ) => EMBED_URLS.some( ( u ) => url.href.includes( u ) ), + async ( route ) => { + await route.fulfill( { + json: MOCK_RESPONSES, + } ); + } + ); + + await editor.insertBlock( { + name: 'test/test-inner-blocks-locking-all-embed', + } ); + await page + .getByRole( 'textbox', { name: 'Embed URL' } ) + .fill( 'https://twitter.com/wordpress' ); + await page.keyboard.press( 'Enter' ); + + await expect( + page.getByRole( 'document', { name: 'Block: Twitter' } ) + ).toBeVisible(); + } ); +} ); 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/editor/various/multi-entity-saving.spec.js b/test/e2e/specs/editor/various/multi-entity-saving.spec.js new file mode 100644 index 00000000000000..7a7298c137c4b5 --- /dev/null +++ b/test/e2e/specs/editor/various/multi-entity-saving.spec.js @@ -0,0 +1,210 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +test.describe( 'Editor - Multi-entity save flow', () => { + let originalSiteTitle, originalBlogDescription; + + test.beforeEach( async ( { requestUtils } ) => { + const siteSettings = await requestUtils.getSiteSettings(); + + originalSiteTitle = siteSettings.title; + originalBlogDescription = siteSettings.description; + } ); + + test.afterEach( async ( { requestUtils, editor } ) => { + await requestUtils.updateSiteSettings( { + title: originalSiteTitle, + description: originalBlogDescription, + } ); + + // Restore the Publish sidebar. + await editor.setPreferences( 'core/edit-post', { + isPublishSidebarEnabled: true, + } ); + } ); + + test( 'Save flow should work as expected', async ( { + admin, + editor, + page, + } ) => { + await admin.createNewPost(); + await editor.canvas + .getByRole( 'textbox', { name: 'Add title' } ) + .fill( 'Test Post...' ); + await page.keyboard.press( 'Enter' ); + + const topBar = page.getByRole( 'region', { name: 'Editor top bar' } ); + const publishButton = topBar.getByRole( 'button', { name: 'Publish' } ); + + // Should not trigger multi-entity save button with only post edited. + await expect( publishButton ).toBeEnabled(); + await expect( publishButton ).not.toHaveClass( /has-changes-dot/ ); + + const openPublishPanel = page.getByRole( 'button', { + name: 'Open publish panel', + } ); + const openSavePanel = page.getByRole( 'button', { + name: 'Open save panel', + } ); + const publishPanel = page.getByRole( 'region', { + name: 'Editor publish', + } ); + + // Should only have publish panel a11y button active with only post edited. + await expect( openPublishPanel ).toBeVisible(); + await expect( openSavePanel ).toBeHidden(); + await expect( publishPanel ).not.toContainText( + 'Are you ready to publish?' + ); + await expect( publishPanel ).not.toContainText( + 'Are you ready to save?' + ); + + // Add a title block and edit it. + await editor.insertBlock( { + name: 'core/site-title', + } ); + const siteTitleField = editor.canvas.getByRole( 'textbox', { + name: 'Site title text', + } ); + await siteTitleField.fill( `${ originalSiteTitle }...` ); + + // Should trigger multi-entity save button once template part edited. + await expect( publishButton ).toHaveClass( /has-changes-dot/ ); + + // Should only have save panel a11y button active after child entities edited. + await expect( openPublishPanel ).toBeHidden(); + await expect( openSavePanel ).toBeVisible(); + await expect( publishPanel ).not.toContainText( + 'Are you ready to publish?' + ); + await expect( publishPanel ).not.toContainText( + 'Are you ready to save?' + ); + + // Opening panel has boxes checked by default. + await publishButton.click(); + await expect( publishPanel ).toContainText( 'Are you ready to save?' ); + const allCheckboxes = await publishPanel + .getByRole( 'checkbox' ) + .count(); + await expect( + publishPanel.getByRole( 'checkbox', { checked: true } ) + ).toHaveCount( allCheckboxes ); + + // Should not show other panels (or their a11y buttons) while save panel opened. + await expect( openPublishPanel ).toBeHidden(); + await expect( openSavePanel ).toBeHidden(); + await expect( publishPanel ).not.toContainText( + 'Are you ready to publish?' + ); + + // Publish panel should open after saving. + await publishPanel.getByRole( 'button', { name: 'Save' } ).click(); + await expect( publishPanel ).toContainText( + 'Are you ready to publish?' + ); + + // No other panels (or their a11y buttons) should be present with publish panel open. + await expect( openPublishPanel ).toBeHidden(); + await expect( openSavePanel ).toBeHidden(); + await expect( publishPanel ).not.toContainText( + 'Are you ready to save?' + ); + + // Close publish panel. + await publishPanel.getByRole( 'button', { name: 'Cancel' } ).click(); + + // Verify saving is disabled. + await expect( + topBar.getByRole( 'button', { name: 'Saved' } ) + ).toBeDisabled(); + await expect( publishButton ).not.toHaveClass( /has-changes-dot/ ); + await expect( openSavePanel ).toBeHidden(); + + await editor.publishPost(); + + // Update the post. + await editor.canvas + .getByRole( 'textbox', { name: 'Add title' } ) + .fill( 'Updated post title' ); + + const updateButton = topBar.getByRole( 'button', { name: 'Update' } ); + + // Verify update button is enabled. + await expect( updateButton ).toBeEnabled(); + + // Verify multi-entity saving not enabled. + await expect( updateButton ).not.toHaveClass( /has-changes-dot/ ); + await expect( openSavePanel ).toBeHidden(); + + await siteTitleField.fill( `${ originalSiteTitle }!` ); + + // Multi-entity saving should be enabled. + await expect( updateButton ).toHaveClass( /has-changes-dot/ ); + await expect( openSavePanel ).toBeVisible(); + } ); + + test( 'Site blocks should save individually', async ( { + admin, + editor, + page, + } ) => { + await admin.createNewPost(); + await editor.setPreferences( 'core/edit-post', { + isPublishSidebarEnabled: false, + } ); + + // Add site blocks. + await editor.insertBlock( { + name: 'core/site-title', + } ); + await editor.insertBlock( { + name: 'core/site-tagline', + } ); + + const siteTitleField = editor.canvas.getByRole( 'textbox', { + name: 'Site title text', + } ); + + // Ensure title is retrieved before typing. + await expect( siteTitleField ).toHaveText( originalSiteTitle ); + + await siteTitleField.fill( `${ originalSiteTitle }...` ); + await editor.canvas + .getByRole( 'document', { + name: 'Block: Site Tagline', + } ) + .fill( 'Just another WordPress site' ); + + const topBar = page.getByRole( 'region', { name: 'Editor top bar' } ); + const publishPanel = page.getByRole( 'region', { + name: 'Editor publish', + } ); + + await topBar.getByRole( 'button', { name: 'Publish' } ).click(); + await expect( publishPanel.getByRole( 'checkbox' ) ).toHaveCount( 3 ); + + // Skip site title saving. + await publishPanel + .getByRole( 'checkbox', { + name: 'Title', + } ) + .setChecked( false ); + + await publishPanel.getByRole( 'button', { name: 'Save' } ).click(); + + // Wait for the snackbar notice that the post has been published. + await page + .getByRole( 'button', { name: 'Dismiss this notice' } ) + .filter( { hasText: 'published' } ) + .waitFor(); + + await topBar.getByRole( 'button', { name: 'Update' } ).click(); + + await expect( publishPanel.getByRole( 'checkbox' ) ).toHaveCount( 1 ); + } ); +} ); 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/e2e/specs/site-editor/site-editor-export.spec.js b/test/e2e/specs/site-editor/site-editor-export.spec.js new file mode 100644 index 00000000000000..a0a56c18089cc2 --- /dev/null +++ b/test/e2e/specs/site-editor/site-editor-export.spec.js @@ -0,0 +1,38 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +test.describe( 'Site Editor Templates Export', () => { + test.beforeAll( async ( { requestUtils } ) => { + await Promise.all( [ + requestUtils.activateTheme( 'emptytheme' ), + requestUtils.deleteAllTemplates( 'wp_template' ), + requestUtils.deleteAllTemplates( 'wp_template_part' ), + ] ); + } ); + + test.afterAll( async ( { requestUtils } ) => { + await requestUtils.activateTheme( 'twentytwentyone' ); + } ); + + test( 'clicking export should download emptytheme.zip file', async ( { + admin, + page, + } ) => { + await admin.visitSiteEditor( { + postId: 'emptytheme//index', + postType: 'wp_template', + canvas: 'edit', + } ); + await page + .getByRole( 'region', { name: 'Editor top bar' } ) + .getByRole( 'button', { name: 'Options' } ) + .click(); + + const promise = page.waitForEvent( 'download' ); + await page.getByRole( 'menuitem', { name: 'Export' } ).click(); + const download = await promise; + expect( download.suggestedFilename() ).toBe( 'emptytheme.zip' ); + } ); +} ); diff --git a/test/e2e/specs/site-editor/user-global-styles-revisions.spec.js b/test/e2e/specs/site-editor/user-global-styles-revisions.spec.js index 61fbf7c795a60b..2d51b5ac5014b8 100644 --- a/test/e2e/specs/site-editor/user-global-styles-revisions.spec.js +++ b/test/e2e/specs/site-editor/user-global-styles-revisions.spec.js @@ -147,9 +147,6 @@ class UserGlobalStylesRevisions { .getByRole( 'menubar', { name: 'Styles actions' } ) .click(); await this.page.getByRole( 'button', { name: 'Revisions' } ).click(); - await this.page - .getByRole( 'menuitem', { name: /^Revision history/ } ) - .click(); } async openStylesPanel() { 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/test/native/jest.config.js b/test/native/jest.config.js index ad5c794ebbce88..4859ea597e0f63 100644 --- a/test/native/jest.config.js +++ b/test/native/jest.config.js @@ -24,7 +24,6 @@ const transpiledPackageNames = glob( 'packages/*/src/index.{js,ts}' ).map( const RAW_HANDLING_UNSUPPORTED_UNIT_TESTS = [ 'html-formatting-remover', 'phrasing-content-reducer', - 'ms-list-converter', 'figure-content-reducer', 'special-comment-converter', 'normalise-blocks', 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" },