diff --git a/.github/workflows/performance.yml b/.github/workflows/performance.yml index 98615b93b8a176..9bfb9f912849af 100644 --- a/.github/workflows/performance.yml +++ b/.github/workflows/performance.yml @@ -47,9 +47,9 @@ jobs: [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" nvm -v - - name: Compare performance with trunk + - name: Compare performance with base branch if: github.event_name == 'pull_request' - run: ./bin/plugin/cli.js perf $GITHUB_SHA trunk --tests-branch $GITHUB_SHA + run: ./bin/plugin/cli.js perf $GITHUB_SHA ${{ github.base_ref }} --tests-branch $GITHUB_SHA - name: Compare performance with current WordPress Core and previous Gutenberg versions if: github.event_name == 'release' diff --git a/.github/workflows/pull-request-automation.yml b/.github/workflows/pull-request-automation.yml index 996bb1667ada57..a9101ad886b4d5 100644 --- a/.github/workflows/pull-request-automation.yml +++ b/.github/workflows/pull-request-automation.yml @@ -17,24 +17,8 @@ jobs: ref: trunk show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} - - name: Use desired version of Node.js - uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 - with: - node-version-file: '.nvmrc' - check-latest: true - - - name: Cache NPM packages - uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 - with: - # npm cache files are stored in `~/.npm` on Linux/macOS - path: ~/.npm - key: ${{ runner.os }}-node-${{ matrix.node }}-npm-pr-automation-cache-${{ hashFiles('**/package-lock.json') }} - - # Changing into the action's directory and running `npm install` is much - # faster than a full project-wide `npm ci`. - - name: Install NPM dependencies - run: npm install - working-directory: packages/project-management-automation + - name: Setup Node.js and install dependencies + uses: ./.github/setup-node - uses: ./packages/project-management-automation with: diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index c0f70070908c1c..b80804b824e3cd 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -135,7 +135,9 @@ jobs: uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 with: name: build-assets - path: ./build/ + path: | + ./build/ + ./build-module/ test-php: name: PHP ${{ matrix.php }}${{ matrix.multisite && ' multisite' || '' }}${{ matrix.wordpress != '' && format( ' (WP {0}) ', matrix.wordpress ) || '' }} on ubuntu-latest @@ -212,7 +214,6 @@ jobs: uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: name: build-assets - path: ./build - name: Docker debug information run: | diff --git a/backport-changelog/6.7/7360.md b/backport-changelog/6.7/7360.md new file mode 100644 index 00000000000000..b2fb8efd624b93 --- /dev/null +++ b/backport-changelog/6.7/7360.md @@ -0,0 +1,3 @@ +https://github.com/WordPress/wordpress-develop/pull/7360 + +* https://github.com/WordPress/gutenberg/pull/65460 diff --git a/backport-changelog/6.7/7543.md b/backport-changelog/6.7/7543.md new file mode 100644 index 00000000000000..7dcb74354ac813 --- /dev/null +++ b/backport-changelog/6.7/7543.md @@ -0,0 +1,3 @@ +https://github.com/WordPress/wordpress-develop/pull/7543 + +* https://github.com/WordPress/gutenberg/pull/65958 diff --git a/backport-changelog/6.7/7552.md b/backport-changelog/6.7/7552.md new file mode 100644 index 00000000000000..d304162b3cae67 --- /dev/null +++ b/backport-changelog/6.7/7552.md @@ -0,0 +1,3 @@ +https://github.com/WordPress/wordpress-develop/pull/7552 + +* https://github.com/WordPress/gutenberg/pull/66058 diff --git a/backport-changelog/6.7/7661.md b/backport-changelog/6.7/7661.md new file mode 100644 index 00000000000000..2170f50c2a4e86 --- /dev/null +++ b/backport-changelog/6.7/7661.md @@ -0,0 +1,3 @@ +https://github.com/WordPress/wordpress-develop/pull/7661 + +* https://github.com/WordPress/gutenberg/pull/66468 diff --git a/changelog.txt b/changelog.txt index b0e6c8e907582e..dca31f9afc622e 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,276 @@ == Changelog == += 19.3.0-rc.1 = + +## Changelog + +### Features + +#### Zoom Out +- Remove experimental flag. ([65404](https://github.com/WordPress/gutenberg/pull/65404)) + +### Enhancements + +- Create Block: Update the minimum required PHP version to 7.2. ([65166](https://github.com/WordPress/gutenberg/pull/65166)) +- DataViews: remove unused `.dataviews-view-table__cell-content-wrapper:Empty` style rule. ([65084](https://github.com/WordPress/gutenberg/pull/65084)) +- Media Utils: Add TypeScript support and export more utils. ([64784](https://github.com/WordPress/gutenberg/pull/64784)) +- Media placeholders: Add "drag" to the text. ([65149](https://github.com/WordPress/gutenberg/pull/65149)) +- Restore: Move to trash button in Document settings. ([65087](https://github.com/WordPress/gutenberg/pull/65087)) +- Inspector Controls: Use custom block name in inspector controls when available. ([65398](https://github.com/WordPress/gutenberg/pull/65398)) +- Plugin: Don't force iframe editor when gutenberg plugin and block theme are enabled. ([65372](https://github.com/WordPress/gutenberg/pull/65372)) +- Icons: Adds bell and bell-unread icons. ([65324](https://github.com/WordPress/gutenberg/pull/65324)) +- Editor topbar: Reorder the actions on the right. ([65163](https://github.com/WordPress/gutenberg/pull/65163)) +- Patterns: Add opt out preference to the 'Choose a Pattern' modal when adding a page. ([65026](https://github.com/WordPress/gutenberg/pull/65026)) +- Locked Templates: Blocks with contentOnly locking should not be transformable. ([64917](https://github.com/WordPress/gutenberg/pull/64917)) +- Block Locking: Add border to Replace item in content only image toolbar. ([64849](https://github.com/WordPress/gutenberg/pull/64849)) + +#### Components +- Styling: Apply elevation scale in components package. ([65159](https://github.com/WordPress/gutenberg/pull/65159)) +- Tabs: Improve Tabs indicator animation and related utils. ([64926](https://github.com/WordPress/gutenberg/pull/64926)) +- Modal + - Add exit animation. ([65203](https://github.com/WordPress/gutenberg/pull/65203)) + - Decrease close button size. ([65131](https://github.com/WordPress/gutenberg/pull/65131)) +- Navigator Screen: Warn if path doesn't follow a URL-like scheme. ([65231](https://github.com/WordPress/gutenberg/pull/65231)) +- Card: Update Card radius. ([65053](https://github.com/WordPress/gutenberg/pull/65053)) +- Combobox Control: Add placeholder attribute. ([65254](https://github.com/WordPress/gutenberg/pull/65254)) + +#### Block Library +- Allow dropping multiple images to the image block. ([65030](https://github.com/WordPress/gutenberg/pull/65030)) +- Categories List block: Add dropdown for taxonomies. ([65272](https://github.com/WordPress/gutenberg/pull/65272)) +- Image: Adds the block controls for uploading image. ([64320](https://github.com/WordPress/gutenberg/pull/64320)) +- Remove colons from control labels. ([65205](https://github.com/WordPress/gutenberg/pull/65205)) +- Terms List block: Add Categories-specific variation. ([65434](https://github.com/WordPress/gutenberg/pull/65434)) + +#### Zoom Out +- Add Zoom Out toggle to editor header when experiment enabled. ([65183](https://github.com/WordPress/gutenberg/pull/65183)) +- Add prompt for drag and drop in Patterns tab in Zoom Out mode. ([65115](https://github.com/WordPress/gutenberg/pull/65115)) +- Close inserter on exiting Zoom Out to edit. ([65194](https://github.com/WordPress/gutenberg/pull/65194)) +- Show top level sections in List View. ([65202](https://github.com/WordPress/gutenberg/pull/65202)) +- Try vertical displacement when dragging a pattern between existing patterns/sections. ([63896](https://github.com/WordPress/gutenberg/pull/63896)) + +#### Block Editor +- Link Editing: Automatically add tel to phone number when linking URL. ([64865](https://github.com/WordPress/gutenberg/pull/64865)) +thub.com/WordPress/gutenberg/pull/65300)) +- Drag and Drop: When dragging a mix of video, audio, and image blocks, create individual blocks as appropriate. ([65144](https://github.com/WordPress/gutenberg/pull/65144)) +- URLInput: Replace input with InputControl. ([65158](https://github.com/WordPress/gutenberg/pull/65158)) +- Normalize block inspector controls spacing. ([64526](https://github.com/WordPress/gutenberg/pull/64526)) + +#### Post Editor +- Add new Media section to preferences modal. ([64846](https://github.com/WordPress/gutenberg/pull/64846)) +- DocumentBar: Replace icon with post type label. ([65170](https://github.com/WordPress/gutenberg/pull/65170)) +- Page editor: Double-click to edit template part. ([65024](https://github.com/WordPress/gutenberg/pull/65024)) +- Post publish upload media dialog: Handle more block types. ([65122](https://github.com/WordPress/gutenberg/pull/65122)) + +#### Block bindings +- Populate block context with inherited post type from template slug. ([65062](https://github.com/WordPress/gutenberg/pull/65062)) +- Try gap 0 on attribute items. ([65277](https://github.com/WordPress/gutenberg/pull/65277)) +- Use post meta label from `register_meta` in block bindings workflows. ([65099](https://github.com/WordPress/gutenberg/pull/65099)) + +#### Global Styles +- Refactor site background controls and move site global styles into Background group. ([65304](https://github.com/WordPress/gutenberg/pull/65304)) +- Spacing control: Replace sides dropdwon with link button. ([65193](https://github.com/WordPress/gutenberg/pull/65193)) + +#### Data Views +- DataViews Sidebar: Display item count on DataViews sidebar. ([65223](https://github.com/WordPress/gutenberg/pull/65223)) +- DataViews: Improve UX of bundled views for Pages. ([65295](https://github.com/WordPress/gutenberg/pull/65295)) + +#### Interactivity API +- Refactor context proxies. ([64713](https://github.com/WordPress/gutenberg/pull/64713)) +- Update: Rephrase "Force page reload" and move to Advanced. ([65081](https://github.com/WordPress/gutenberg/pull/65081)) + +#### REST API +- Global Styles: Allow read access to users with `edit_posts` capabilities. ([65071](https://github.com/WordPress/gutenberg/pull/65071)) +- Query loop / Post template: Enable post format filter. ([64167](https://github.com/WordPress/gutenberg/pull/64167)) + +### New APIs +- Add @wordpress/fields package. + - Introduce the package. ([65230](https://github.com/WordPress/gutenberg/pull/65230)) + - Make the package private. ([65269](https://github.com/WordPress/gutenberg/pull/65269)) + +### Bug Fixes + +- Align popover alt variant styling with block toolbar. ([65263](https://github.com/WordPress/gutenberg/pull/65263)) +- Compose: Correctly call timer cleanup in 'useFocusOnMount'. ([65184](https://github.com/WordPress/gutenberg/pull/65184)) +- Fix some docblock types related to the Template Registration API. ([65187](https://github.com/WordPress/gutenberg/pull/65187)) +- Fix the issue where block spacing control not shown. ([65371](https://github.com/WordPress/gutenberg/pull/65371)) +- Fix unintentional block toolbar shadow. ([65182](https://github.com/WordPress/gutenberg/pull/65182)) +- Fix: Moving a page to the trash on the site editor does not goes back to the pages list. ([65119](https://github.com/WordPress/gutenberg/pull/65119)) +- Fix: Moving the last page item to the the trash causes a crash. ([65236](https://github.com/WordPress/gutenberg/pull/65236)) +- Preferences: Fix back button on mobile. ([65141](https://github.com/WordPress/gutenberg/pull/65141)) +- Revert "Don't force iframe editor when gutenberg plugin and block the me are enabled (#65372)". ([65402](https://github.com/WordPress/gutenberg/pull/65402)) +- Post Summary Panel: Restore `height:Auto` for toggle buttons. ([65362](https://github.com/WordPress/gutenberg/pull/65362)) +- Fix Tabs styling in Font Library modal. ([65330](https://github.com/WordPress/gutenberg/pull/65330)) +- E2E: Change deprecated social icons for standard in end-to-end. ([65312](https://github.com/WordPress/gutenberg/pull/65312)) +- Typography: Make title blocks apply typographic styles consistently. ([65307](https://github.com/WordPress/gutenberg/pull/65307)) +- Target Hints REST API: Add missing param sanitization. ([65280](https://github.com/WordPress/gutenberg/pull/65280)) +- Interactivity API: Update iterable signals when `deepMerge()` adds new properties. ([65135](https://github.com/WordPress/gutenberg/pull/65135)) +- Navigation Menus: Typography styling support to the navigation submenu block. ([65060](https://github.com/WordPress/gutenberg/pull/65060)) +- Grid: In RTL languages, the resize handles point in the opposite direction. ([64995](https://github.com/WordPress/gutenberg/pull/64995)) +- Block Locking: Fix Content Only Toolbar icon focus style. ([64940](https://github.com/WordPress/gutenberg/pull/64940)) +- Image: Fix resizing to max width in classic themes. ([64819](https://github.com/WordPress/gutenberg/pull/64819)) +- Meta Boxes: Try split content view. ([64351](https://github.com/WordPress/gutenberg/pull/64351)) +- Distraction Free: Fix blurry edge along editor header. ([64277](https://github.com/WordPress/gutenberg/pull/64277)) + +#### Block Library +- Comments Pagination: Fix warning returned by comments pagination blocks. ([65435](https://github.com/WordPress/gutenberg/pull/65435)) +- Cover: Explicitly set isUserOverlayColor to false when media is updated. ([65105](https://github.com/WordPress/gutenberg/pull/65105)) +- Disallow setting grid block rows/columns to zero. ([65217](https://github.com/WordPress/gutenberg/pull/65217)) +- Fix image block crash. ([65222](https://github.com/WordPress/gutenberg/pull/65222)) +- Fix: Buttons block: Block spacing value does not apply to both vertical and horizontal alignment. ([64971](https://github.com/WordPress/gutenberg/pull/64971)) +- Fix: Embed blocks: Figcaption inserted via toolbar not nested within figure element - #64960. ([64970](https://github.com/WordPress/gutenberg/pull/64970)) +- Image cropping: Skip making an API request if there are no changes to apply. ([65384](https://github.com/WordPress/gutenberg/pull/65384)) +- Comments Pagination: Pass the comments query `paged` arg to functions `get_next_comments_link` and `get_previous_comments_link`. ([63698](https://github.com/WordPress/gutenberg/pull/63698)) +- Query Loop: Default to querying posts when on singular content. ([65067](https://github.com/WordPress/gutenberg/pull/65067)) + +#### Block Editor +- Inserter: Fix loading indicator for reusable blocks. ([64839](https://github.com/WordPress/gutenberg/pull/64839)) +- Normalize spacing in Layout hook controls. ([65132](https://github.com/WordPress/gutenberg/pull/65132)) +- Pattern Inserter: Fix pattern list overflow. ([65192](https://github.com/WordPress/gutenberg/pull/65192)) +- Remove reset styles RTL from the iframe. ([65150](https://github.com/WordPress/gutenberg/pull/65150)) +- Revert "Block Insertion: Clear the insertion point when selecting a d…. ([65208](https://github.com/WordPress/gutenberg/pull/65208)) + +#### Components +- BoxControl: Unify input filed width whether linked or not. ([65348](https://github.com/WordPress/gutenberg/pull/65348)) +- ComboboxControl: Add more unit tests. ([65255](https://github.com/WordPress/gutenberg/pull/65255)) +- Fix: Button Replace remaining 40px default size violations [Edit widgets]. ([65367](https://github.com/WordPress/gutenberg/pull/65367)) +- Tabs: Fix vertical indicator. ([65385](https://github.com/WordPress/gutenberg/pull/65385)) + +#### Block bindings +- Fix empty strings placeholders in post meta bindings. ([65089](https://github.com/WordPress/gutenberg/pull/65089)) +- Prioritize existing `placeholder` over `bindingsPlaceholder`. ([65154](https://github.com/WordPress/gutenberg/pull/65154)) +- Revert "Block Bindings: Prioritize existing `placeholder` over `bindingsPlaceholder`". ([65190](https://github.com/WordPress/gutenberg/pull/65190)) + +#### Zoom Out +- Force device type to Desktop whenever zoom out is invoked. ([64476](https://github.com/WordPress/gutenberg/pull/64476)) +- Hide toolbar icon on smaller viewports. ([65437](https://github.com/WordPress/gutenberg/pull/65437)) +- Remove zoom out toggle when editor is not iframed. ([65452](https://github.com/WordPress/gutenberg/pull/65452)) + +### Accessibility + +- A11y: Add script-module. ([65101](https://github.com/WordPress/gutenberg/pull/65101)) +- Interactivity API: Use a11y Script Module in Gutenberg. ([65123](https://github.com/WordPress/gutenberg/pull/65123)) +- Script Modules API: Print script module live regions HTML in page HTML. ([65380](https://github.com/WordPress/gutenberg/pull/65380)) +- Post Editor: Support keyboard resizing of meta boxes pane. ([65325](https://github.com/WordPress/gutenberg/pull/65325)) +- DatePicker: Better hover/focus styles. ([65117](https://github.com/WordPress/gutenberg/pull/65117)) +- Form Input: Don't use `flex-direction: Row-reverse` for checkbox field. ([64232](https://github.com/WordPress/gutenberg/pull/64232)) +- Navigation Menus: Remove Warning and add notice for Navigation. ([63921](https://github.com/WordPress/gutenberg/pull/63921)) +- Global Styles: Fix the shadows Range control accessibility and usability. ([63908](https://github.com/WordPress/gutenberg/pull/63908)) +- Block Editor: Fix accessibility of the hooked blocks toggles. ([63133](https://github.com/WordPress/gutenberg/pull/63133)) + + +### Performance + +- Core Data: Batch remaining actions in resolvers. ([65176](https://github.com/WordPress/gutenberg/pull/65176)) +- Block Editor: Use static access for selector in 'useZoomOutModeExit'. ([65337](https://github.com/WordPress/gutenberg/pull/65337)) +- Editor: Optimize global styles permission check. ([65177](https://github.com/WordPress/gutenberg/pull/65177)) + + +### Experiments + +- Block bindings REST API: Bring bindings UI in Site Editor. ([64072](https://github.com/WordPress/gutenberg/pull/64072)) + + +### Documentation + +- Add JSDoc block for getSectionRootClientId in block editor package. ([65219](https://github.com/WordPress/gutenberg/pull/65219)) +- ButtonGroup: Fix story to show what the component does. ([65336](https://github.com/WordPress/gutenberg/pull/65336)) +- DataViews storybook + - Better styles for combined fields story. ([65078](https://github.com/WordPress/gutenberg/pull/65078)) + - Enable all layouts for combined fields storybook. ([65082](https://github.com/WordPress/gutenberg/pull/65082)) +- Docs: Fix minor typos in Build your first block tutorial. ([64961](https://github.com/WordPress/gutenberg/pull/64961)) +- Docs: Update the content of the API version 3 section in the Block API Reference. ([65375](https://github.com/WordPress/gutenberg/pull/65375)) +- Fix typo in Slot Fills documentation. ([65275](https://github.com/WordPress/gutenberg/pull/65275)) + + +### Code Quality + +- Components: Transition to the new 40px default size. + - Button: + - Add __next40pxDefaultSize for files in editor 3. ([65139](https://github.com/WordPress/gutenberg/pull/65139)) + - Add __next40pxDefaultSize for files in editor 4. ([65140](https://github.com/WordPress/gutenberg/pull/65140)) + - Add props for buttons in editor 1. ([65068](https://github.com/WordPress/gutenberg/pull/65068)) + - Add props for buttons in editor 2. ([65083](https://github.com/WordPress/gutenberg/pull/65083)) + - Fix: Replace remaining 40px default size violations [Block Editor 4]. ([65257](https://github.com/WordPress/gutenberg/pull/65257)) + - Fix: Replace remaining 40px default size violation [Block library 3]. ([65110](https://github.com/WordPress/gutenberg/pull/65110)) + - Fix: Replace remaining 40px default size violation [Block library 4]. ([65143](https://github.com/WordPress/gutenberg/pull/65143)) + - Fix: Replace remaining 40px default size violation [Block library]. ([65075](https://github.com/WordPress/gutenberg/pull/65075)) + - Fix: Replace remaining 40px default size violation [Edit Site 2]. ([65258](https://github.com/WordPress/gutenberg/pull/65258)) + - Fix: Replace remaining 40px default size violations [Block library 1]. ([65033](https://github.com/WordPress/gutenberg/pull/65033)) + - Fix: Replace remaining 40px default size violations [Block Editor 1]. ([65034](https://github.com/WordPress/gutenberg/pull/65034)) + - BoxControl + - Add lint rule for 40px size prop usage. ([65341](https://github.com/WordPress/gutenberg/pull/65341)) + - DimensionsPanel: Apply 40px default size to UI when no spacing preset is available. ([65300](https://gi +- Add `useEvent` and revamped `useResizeObserver` to `@wordpress/compose`. ([64943](https://github.com/WordPress/gutenberg/pull/64943)) +- DataViews: Use Dropdown for views configuration dialog. ([65314](https://github.com/WordPress/gutenberg/pull/65314)) +- Platform docs: Upgrade dependencies. ([65445](https://github.com/WordPress/gutenberg/pull/65445)) +- Rename edit-post__fade-in-animation and unify keyframe definitions. ([65377](https://github.com/WordPress/gutenberg/pull/65377)) +- Update minimum required version in PHP. ([65301](https://github.com/WordPress/gutenberg/pull/65301)) +- Editor: Use hooks instead of HoC in `BlockManager`. ([65349](https://github.com/WordPress/gutenberg/pull/65349)) +- Data Views Fields: Migrate store and actions from editor package to fields package. ([65261](https://github.com/WordPress/gutenberg/pull/65261)) +- Plugin: Remove 'function_exists' checks for methods with 'gutenberg' prefix. ([65260](https://github.com/WordPress/gutenberg/pull/65260)) +- Global Styles: Update REST controller override method and backport changes from Core. ([65259](https://github.com/WordPress/gutenberg/pull/65259)) +- Patterns: Remove unused method returned from 'mapSelect'. ([65073](https://github.com/WordPress/gutenberg/pull/65073)) +- Embed: Convert EmbedPreview component to functional component. ([51325](https://github.com/WordPress/gutenberg/pull/51325)) + +#### Components +- BoxControl: Fix critical error when null value is passed. ([65287](https://github.com/WordPress/gutenberg/pull/65287)) +- Composite: + - Deprecate legacy, unstable version. ([63572](https://github.com/WordPress/gutenberg/pull/63572)) + - Remove store prop and useCompositeStore hook. ([64723](https://github.com/WordPress/gutenberg/pull/64723)) + - Stabilize APIs. ([63569](https://github.com/WordPress/gutenberg/pull/63569)) +- `@wordpress/components`: Add local copy of `use-lilius`. ([65097](https://github.com/WordPress/gutenberg/pull/65097)) + +#### Block bindings +- Always prioritize using context in post meta source logic. ([65449](https://github.com/WordPress/gutenberg/pull/65449)) +- Improve getRegisteredPostMeta resolver. ([65450](https://github.com/WordPress/gutenberg/pull/65450)) +- Remove extra filtering of empty sources. ([65447](https://github.com/WordPress/gutenberg/pull/65447)) + +#### Block Editor +- Remove the 'PrivateInserter' component. ([65111](https://github.com/WordPress/gutenberg/pull/65111)) +- Use the tooltip from a button in 'ButtonBlockAppender'. ([65113](https://github.com/WordPress/gutenberg/pull/65113)) +- Remove unused css selectors. ([65276](https://github.com/WordPress/gutenberg/pull/65276)) + +### Tools + +- Scripts: Update stylelint dependency and the default configuration. ([64828](https://github.com/WordPress/gutenberg/pull/64828)) +- Styleling config: Fix stylelint configuration missing files for npm. ([65313](https://github.com/WordPress/gutenberg/pull/65313)) + +#### Build Tooling +- Build Plugin: Simplify and improve zip contents. ([65232](https://github.com/WordPress/gutenberg/pull/65232)) +- Build zip artifact on release and wp production branches. ([65471](https://github.com/WordPress/gutenberg/pull/65471)) +- Build: Include Core blocks' `render` and `variations` files. ([63311](https://github.com/WordPress/gutenberg/pull/63311)) +- Script Modules + - Prepare build for more script modules. ([65064](https://github.com/WordPress/gutenberg/pull/65064)) + - Remove babel from script-modules build. ([65279](https://github.com/WordPress/gutenberg/pull/65279)) + - Remove es-module shims and importmap-polyfill. ([65210](https://github.com/WordPress/gutenberg/pull/65210)) +- Correctly generate PHP files for server-side rendering of blocks on Windows OS. ([65248](https://github.com/WordPress/gutenberg/pull/65248)) +- Packages: Only add polyfills where needed. ([65292](https://github.com/WordPress/gutenberg/pull/65292)) +- Switch from UglifyJS to Terser to build the polyfill script. ([65278](https://github.com/WordPress/gutenberg/pull/65278)) + +#### Testing +- Unit tests: Mock matchMedia to enforce prefers-reduce-motion. ([65438](https://github.com/WordPress/gutenberg/pull/65438)) +- Upgrade Playwright to v1.47. ([65156](https://github.com/WordPress/gutenberg/pull/65156)) + +## First-time contributors + +The following PRs were merged by first-time contributors: + +- @AKSHAT2802: Add __next40pxDefaultSize for files in editor 4. ([65140](https://github.com/WordPress/gutenberg/pull/65140)) +- @devansh016: Automatically add tel to phone number when linking URL. ([64865](https://github.com/WordPress/gutenberg/pull/64865)) +- @dhruvang21: Fix: Button Replace remaining 40px default size violations [Edit widgets]. ([65367](https://github.com/WordPress/gutenberg/pull/65367)) +- @farid-hadi: Docs: Fix minor typos in Build your first block tutorial. ([64961](https://github.com/WordPress/gutenberg/pull/64961)) +- @greenworld: Fix typo in Slot Fills documentation. ([65275](https://github.com/WordPress/gutenberg/pull/65275)) +- @louwie17: Convert EmbedPreview component to functional component. ([51325](https://github.com/WordPress/gutenberg/pull/51325)) +- @rahulharpal1603: URLInput: Replace input with InputControl. ([65158](https://github.com/WordPress/gutenberg/pull/65158)) + + +## Contributors + +The following contributors merged PRs in this release: + +@aaronrobertshaw @afercia @AKSHAT2802 @Aljullu @andrewserong @carolinan @cbravobernal @ciampo @colorful-tones @creativecoder @DaniGuardiola @DAreRodz @devansh016 @dhruvang21 @ellatrix @farid-hadi @getdave @gigitux @greenworld @gziolo @hbhalodia @jameskoster @jasmussen @javierarce @jeryj @jorgefilipecosta @jsnajdr @kevin940726 @louwie17 @madhusudhand @MaggieCabrera @Mamaduka @mikeybinns @mirka @ntsekouras @oandregal @ockham @peterwilsoncc @rahulharpal1603 @ramonjd @richtabor @rohitmathur-7 @SantosGuillamot @scruffian @sgomes @sirreal @stokesman @swissspidy @t-hamano @talldan @vipul0425 @zaguiini + + = 19.2.0 = ## Changelog diff --git a/docs/manifest.json b/docs/manifest.json index e4eba19d99fa29..d7f74d47995b63 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -1697,6 +1697,12 @@ "markdown_source": "../packages/eslint-plugin/README.md", "parent": "packages" }, + { + "title": "@wordpress/fields", + "slug": "packages-fields", + "markdown_source": "../packages/fields/README.md", + "parent": "packages" + }, { "title": "@wordpress/format-library", "slug": "packages-format-library", diff --git a/gutenberg.php b/gutenberg.php index 342c61fb56cd13..8dddcfeccd5282 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.5 * Requires PHP: 7.2 - * Version: 19.2.0 + * Version: 19.3.0-rc.1 * Author: Gutenberg Team * Text Domain: gutenberg * diff --git a/lib/client-assets.php b/lib/client-assets.php index 62e874d6b06c82..2343530e5595a7 100644 --- a/lib/client-assets.php +++ b/lib/client-assets.php @@ -601,6 +601,56 @@ function gutenberg_register_vendor_scripts( $scripts ) { } add_action( 'wp_default_scripts', 'gutenberg_register_vendor_scripts' ); +/** + * Registers or re-registers Gutenberg Script Modules. + * + * Script modules that are registered by Core will be re-registered by Gutenberg. + * + * @since 19.3.0 + */ +function gutenberg_default_script_modules() { + /* + * Expects multidimensional array like: + * + * 'interactivity/index.min.js' => array('dependencies' => array(…), 'version' => '…'), + * 'interactivity/debug.min.js' => array('dependencies' => array(…), 'version' => '…'), + * 'interactivity-router/index.min.js' => … + */ + $assets = include gutenberg_dir_path() . '/build-module/assets.php'; + + foreach ( $assets as $file_name => $script_module_data ) { + /* + * Build the WordPress Script Module ID from the file name. + * Prepend `@wordpress/` and remove extensions and `/index` if present: + * - interactivity/index.min.js => @wordpress/interactivity + * - interactivity/debug.min.js => @wordpress/interactivity/debug + * - block-library/query/view.js => @wordpress/block-library/query/view + */ + $script_module_id = '@wordpress/' . preg_replace( '~(?:/index)?\.min\.js$~D', '', $file_name, 1 ); + switch ( $script_module_id ) { + /* + * Interactivity exposes two entrypoints, "/index" and "/debug". + * "/debug" should replalce "/index" in devlopment. + */ + case '@wordpress/interactivity/debug': + if ( ! SCRIPT_DEBUG ) { + continue 2; + } + $script_module_id = '@wordpress/interactivity'; + break; + case '@wordpress/interactivity': + if ( SCRIPT_DEBUG ) { + continue 2; + } + break; + } + + $path = gutenberg_url( "build-module/{$file_name}" ); + wp_register_script_module( $script_module_id, $path, $script_module_data['dependencies'], $script_module_data['version'] ); + } +} +remove_action( 'wp_default_scripts', 'wp_default_script_modules' ); +add_action( 'wp_default_scripts', 'gutenberg_default_script_modules' ); /* * Always remove the Core action hook while gutenberg_enqueue_stored_styles() exists to avoid styles being printed twice. diff --git a/lib/compat/wordpress-6.7/block-bindings.php b/lib/compat/wordpress-6.7/block-bindings.php index a8f68c0f0f04ea..70ba523ac966ea 100644 --- a/lib/compat/wordpress-6.7/block-bindings.php +++ b/lib/compat/wordpress-6.7/block-bindings.php @@ -6,38 +6,28 @@ */ /** - * Adds the block bindings sources registered in the server to the editor settings. - * - * This allows them to be bootstrapped in the editor. - * - * @param array $settings The block editor settings from the `block_editor_settings_all` filter. - * @return array The editor settings including the block bindings sources. + * Bootstrap the block bindings sources registered in the server. */ -function gutenberg_add_server_block_bindings_sources_to_editor_settings( $editor_settings ) { - // Check if the sources are already exposed in the editor settings. - if ( isset( $editor_settings['blockBindingsSources'] ) ) { - return $editor_settings; - } - - $registered_block_bindings_sources = get_all_registered_block_bindings_sources(); - if ( ! empty( $registered_block_bindings_sources ) ) { - // Initialize array. - $editor_settings['blockBindingsSources'] = array(); - foreach ( $registered_block_bindings_sources as $source_name => $source_properties ) { - // Add source with the label to editor settings. - $editor_settings['blockBindingsSources'][ $source_name ] = array( - 'label' => $source_properties->label, +function gutenberg_bootstrap_server_block_bindings_sources() { + $registered_sources = get_all_registered_block_bindings_sources(); + if ( ! empty( $registered_sources ) ) { + $filtered_sources = array(); + foreach ( $registered_sources as $source ) { + $filtered_sources[] = array( + 'name' => $source->name, + 'label' => $source->label, + 'usesContext' => $source->uses_context, ); - // Add `usesContext` property if exists. - if ( ! empty( $source_properties->uses_context ) ) { - $editor_settings['blockBindingsSources'][ $source_name ]['usesContext'] = $source_properties->uses_context; - } } + $script = sprintf( 'for ( const source of %s ) { ! wp.blocks.getBlockBindingsSource( source.name ) && wp.blocks.registerBlockBindingsSource( source ); }', wp_json_encode( $filtered_sources ) ); + wp_add_inline_script( + 'wp-blocks', + $script + ); } - return $editor_settings; } -add_filter( 'block_editor_settings_all', 'gutenberg_add_server_block_bindings_sources_to_editor_settings', 10 ); +add_action( 'enqueue_block_editor_assets', 'gutenberg_bootstrap_server_block_bindings_sources', 5 ); /** * Initialize `canUpdateBlockBindings` editor setting if it doesn't exist. By default, it is `true` only for admin users. diff --git a/lib/compat/wordpress-6.7/block-templates.php b/lib/compat/wordpress-6.7/block-templates.php index d1f2859070b8b4..65d99ee978efed 100644 --- a/lib/compat/wordpress-6.7/block-templates.php +++ b/lib/compat/wordpress-6.7/block-templates.php @@ -5,7 +5,7 @@ * @package gutenberg */ -if ( ! function_exists( 'wp_register_block_template' ) ) { +if ( ! function_exists( 'register_block_template' ) ) { /** * Register a template. * @@ -22,20 +22,63 @@ * } * @return WP_Block_Template|WP_Error The registered template object on success, WP_Error object on failure. */ - function wp_register_block_template( $template_name, $args = array() ) { + function register_block_template( $template_name, $args = array() ) { return WP_Block_Templates_Registry::get_instance()->register( $template_name, $args ); } } +if ( ! function_exists( 'unregister_block_template' ) ) { + /** + * Unregister a template. + * + * @param string $template_name Template name in the form of `plugin_uri//template_name`. + * @return WP_Block_Template|WP_Error The unregistered template object on success, WP_Error object on failure or if + * the template doesn't exist. + */ + function unregister_block_template( $template_name ) { + return WP_Block_Templates_Registry::get_instance()->unregister( $template_name ); + } +} + +if ( ! function_exists( 'wp_register_block_template' ) ) { + /** + * Register a template. + * + * @deprecated 19.4.0 wp_register_block_template is deprecated. Please use register_block_template instead. + * + * @param string $template_name Template name in the form of `plugin_uri//template_name`. + * @param array|string $args { + * Optional. Array or string of arguments for registering a block template. + * + * @type string $title Optional. Title of the template as it will be shown in the Site Editor + * and other UI elements. + * @type string $description Optional. Description of the template as it will be shown in the Site + * Editor. + * @type string $content Optional. Default content of the template that will be used when the + * template is rendered or edited in the editor. + * @type string[] $post_types Optional. Array of post types to which the template should be available. + * @type string $plugin Uri of the plugin that registers the template. + * } + * @return WP_Block_Template|WP_Error The registered template object on success, WP_Error object on failure. + */ + function wp_register_block_template( $template_name, $args = array() ) { + _deprecated_function( __FUNCTION__, 'Gutenberg 19.4.0', 'register_block_template' ); + register_block_template( $template_name, $args ); + } +} + if ( ! function_exists( 'wp_unregister_block_template' ) ) { /** * Unregister a template. * + * @deprecated 19.4.0 wp_unregister_block_template is deprecated. Please use unregister_block_template instead. + * * @param string $template_name Template name in the form of `plugin_uri//template_name`. * @return WP_Block_Template|WP_Error The unregistered template object on success, WP_Error object on failure or if * the template doesn't exist. */ function wp_unregister_block_template( $template_name ) { - return WP_Block_Templates_Registry::get_instance()->unregister( $template_name ); + _deprecated_function( __FUNCTION__, 'Gutenberg 19.4.0', 'unregister_block_template' ); + return unregister_block_template( $template_name ); } } diff --git a/lib/compat/wordpress-6.7/rest-api.php b/lib/compat/wordpress-6.7/rest-api.php index 313367594caae0..89efdc4c4219d2 100644 --- a/lib/compat/wordpress-6.7/rest-api.php +++ b/lib/compat/wordpress-6.7/rest-api.php @@ -50,6 +50,25 @@ function gutenberg_block_editor_preload_paths_6_7( $paths, $context ) { } } + // Preload theme and global styles paths. + $excluded_paths = array(); + if ( 'core/edit-site' === $context->name || 'core/edit-post' === $context->name ) { + $active_theme = get_stylesheet(); + $global_styles_id = WP_Theme_JSON_Resolver_Gutenberg::get_user_global_styles_post_id(); + $paths[] = '/wp/v2/global-styles/themes/' . $active_theme . '?context=view'; + $paths[] = '/wp/v2/global-styles/themes/' . $active_theme . '/variations?context=view'; + $paths[] = array( '/wp/v2/global-styles/' . $global_styles_id, 'OPTIONS' ); + + // Remove duplicate or unnecessary global styles paths. + $excluded_paths[] = '/wp/v2/global-styles/themes/' . $active_theme; + $excluded_paths[] = '/wp/v2/global-styles/' . $global_styles_id; + foreach ( $paths as $key => $path ) { + if ( in_array( $path, $excluded_paths, true ) ) { + unset( $paths[ $key ] ); + } + } + } + return $paths; } add_filter( 'block_editor_rest_api_preload_paths', 'gutenberg_block_editor_preload_paths_6_7', 10, 2 ); diff --git a/lib/compat/wordpress-6.7/script-modules.php b/lib/compat/wordpress-6.7/script-modules.php index 0a440ec81688d2..2282a3d4bd5acd 100644 --- a/lib/compat/wordpress-6.7/script-modules.php +++ b/lib/compat/wordpress-6.7/script-modules.php @@ -102,3 +102,46 @@ function () { }, 20 ); + +/** + * Prints HTML for the a11y Script Module. + * + * a11y relies on some DOM elements to use as ARIA live regions. + * Ideally, these elements are part of the initial HTML of the page + * so that accessibility tools can find them and observe updates. + */ +function gutenberg_a11y_script_module_html() { + $a11y_module_available = false; + + $get_marked_for_enqueue = new ReflectionMethod( 'WP_Script_Modules', 'get_marked_for_enqueue' ); + $get_marked_for_enqueue->setAccessible( true ); + $get_import_map = new ReflectionMethod( 'WP_Script_Modules', 'get_import_map' ); + $get_import_map->setAccessible( true ); + + foreach ( array_keys( $get_marked_for_enqueue->invoke( wp_script_modules() ) ) as $id ) { + if ( '@wordpress/a11y' === $id ) { + $a11y_module_available = true; + break; + } + } + if ( ! $a11y_module_available ) { + foreach ( array_keys( $get_import_map->invoke( wp_script_modules() )['imports'] ) as $id ) { + if ( '@wordpress/a11y' === $id ) { + $a11y_module_available = true; + break; + } + } + } + if ( ! $a11y_module_available ) { + return; + } + echo '
' + . '' + . '
' + . '
' + . '
'; +} +if ( ! method_exists( 'WP_Script_Modules', 'print_a11y_script_module_html' ) ) { + add_action( 'wp_footer', 'gutenberg_a11y_script_module_html' ); + add_action( 'admin_footer', 'gutenberg_a11y_script_module_html' ); +} diff --git a/lib/experimental/script-modules.php b/lib/experimental/script-modules.php index f65bc1704dd890..5a14e1418ed6de 100644 --- a/lib/experimental/script-modules.php +++ b/lib/experimental/script-modules.php @@ -200,65 +200,3 @@ function gutenberg_dequeue_module( $module_identifier ) { _deprecated_function( __FUNCTION__, 'Gutenberg 17.6.0', 'wp_dequeue_script_module' ); wp_script_modules()->dequeue( $module_identifier ); } - -/** - * Prints HTML for the a11y Script Module. - * - * a11y relies on some DOM elements to use as ARIA live regions. - * Ideally, these elements are part of the initial HTML of the page - * so that accessibility tools can find them and observe updates. - */ -function gutenberg_a11y_script_module_html() { - $a11y_module_available = false; - - $get_marked_for_enqueue = new ReflectionMethod( 'WP_Script_Modules', 'get_marked_for_enqueue' ); - $get_marked_for_enqueue->setAccessible( true ); - $get_import_map = new ReflectionMethod( 'WP_Script_Modules', 'get_import_map' ); - $get_import_map->setAccessible( true ); - - foreach ( array_keys( $get_marked_for_enqueue->invoke( wp_script_modules() ) ) as $id ) { - if ( '@wordpress/a11y' === $id ) { - $a11y_module_available = true; - break; - } - } - if ( ! $a11y_module_available ) { - foreach ( array_keys( $get_import_map->invoke( wp_script_modules() )['imports'] ) as $id ) { - if ( '@wordpress/a11y' === $id ) { - $a11y_module_available = true; - break; - } - } - } - if ( ! $a11y_module_available ) { - return; - } - echo '
' - . '' - . '
' - . '
' - . '
'; -} - -/** - * Registers Gutenberg Script Modules. - * - * @since 19.3 - */ -function gutenberg_register_script_modules() { - // When in production, use the plugin's version as the default asset version; - // else (for development or test) default to use the current time. - $default_version = defined( 'GUTENBERG_VERSION' ) && ! SCRIPT_DEBUG ? GUTENBERG_VERSION : time(); - - wp_deregister_script_module( '@wordpress/a11y' ); - wp_register_script_module( - '@wordpress/a11y', - gutenberg_url( 'build-module/a11y/index.min.js' ), - array(), - $default_version - ); - - add_action( 'wp_footer', 'gutenberg_a11y_script_module_html' ); - add_action( 'admin_footer', 'gutenberg_a11y_script_module_html' ); -} -add_action( 'init', 'gutenberg_register_script_modules' ); diff --git a/lib/interactivity-api.php b/lib/interactivity-api.php index c00d68bc70e8e2..ff68936f054a7e 100644 --- a/lib/interactivity-api.php +++ b/lib/interactivity-api.php @@ -5,37 +5,6 @@ * @package gutenberg */ -/** - * Deregisters the Core Interactivity API Modules and replace them - * with the ones from the Gutenberg plugin. - */ -function gutenberg_reregister_interactivity_script_modules() { - $default_version = defined( 'GUTENBERG_VERSION' ) && ! SCRIPT_DEBUG ? GUTENBERG_VERSION : time(); - wp_deregister_script_module( '@wordpress/interactivity' ); - wp_deregister_script_module( '@wordpress/interactivity-router' ); - - wp_register_script_module( - '@wordpress/interactivity', - gutenberg_url( '/build-module/' . ( SCRIPT_DEBUG ? 'interactivity/debug.min.js' : 'interactivity/index.min.js' ) ), - array(), - $default_version - ); - - wp_register_script_module( - '@wordpress/interactivity-router', - gutenberg_url( '/build-module/interactivity-router/index.min.js' ), - array( - array( - 'id' => '@wordpress/a11y', - 'import' => 'dynamic', - ), - '@wordpress/interactivity', - ), - $default_version - ); -} -add_action( 'init', 'gutenberg_reregister_interactivity_script_modules' ); - /** * Adds script data to the interactivity-router script module. * diff --git a/package-lock.json b/package-lock.json index dcdc5043ac6d88..1c423c88fb5d26 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "gutenberg", - "version": "19.2.0", + "version": "19.3.0-rc.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "gutenberg", - "version": "19.2.0", + "version": "19.3.0-rc.1", "hasInstallScript": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -52564,7 +52564,7 @@ }, "packages/a11y": { "name": "@wordpress/a11y", - "version": "4.7.0", + "version": "4.8.2", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -52578,7 +52578,7 @@ }, "packages/annotations": { "name": "@wordpress/annotations", - "version": "3.7.0", + "version": "3.8.3", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -52606,7 +52606,7 @@ }, "packages/api-fetch": { "name": "@wordpress/api-fetch", - "version": "7.7.0", + "version": "7.8.2", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -52620,7 +52620,7 @@ }, "packages/autop": { "name": "@wordpress/autop", - "version": "4.7.0", + "version": "4.8.1", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0" @@ -52632,7 +52632,7 @@ }, "packages/babel-plugin-import-jsx-pragma": { "name": "@wordpress/babel-plugin-import-jsx-pragma", - "version": "5.7.0", + "version": "5.8.1", "dev": true, "license": "GPL-2.0-or-later", "engines": { @@ -52645,7 +52645,7 @@ }, "packages/babel-plugin-makepot": { "name": "@wordpress/babel-plugin-makepot", - "version": "6.7.0", + "version": "6.8.1", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -52663,7 +52663,7 @@ }, "packages/babel-preset-default": { "name": "@wordpress/babel-preset-default", - "version": "8.7.0", + "version": "8.8.2", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -52686,7 +52686,7 @@ }, "packages/base-styles": { "name": "@wordpress/base-styles", - "version": "5.7.0", + "version": "5.8.3", "dev": true, "license": "GPL-2.0-or-later", "engines": { @@ -52696,7 +52696,7 @@ }, "packages/blob": { "name": "@wordpress/blob", - "version": "4.7.0", + "version": "4.8.1", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0" @@ -52708,7 +52708,7 @@ }, "packages/block-directory": { "name": "@wordpress/block-directory", - "version": "5.7.0", + "version": "5.8.17", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -52730,7 +52730,8 @@ "@wordpress/plugins": "file:../plugins", "@wordpress/private-apis": "file:../private-apis", "@wordpress/url": "file:../url", - "change-case": "^4.1.2" + "change-case": "^4.1.2", + "clsx": "^2.1.1" }, "engines": { "node": ">=18.12.0", @@ -52743,7 +52744,7 @@ }, "packages/block-editor": { "name": "@wordpress/block-editor", - "version": "14.2.0", + "version": "14.3.14", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -52842,7 +52843,7 @@ }, "packages/block-library": { "name": "@wordpress/block-library", - "version": "9.7.0", + "version": "9.8.15", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -52908,7 +52909,7 @@ }, "packages/block-serialization-default-parser": { "name": "@wordpress/block-serialization-default-parser", - "version": "5.7.0", + "version": "5.8.1", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0" @@ -52920,7 +52921,7 @@ }, "packages/block-serialization-spec-parser": { "name": "@wordpress/block-serialization-spec-parser", - "version": "5.7.0", + "version": "5.8.1", "license": "GPL-2.0-or-later", "dependencies": { "pegjs": "^0.10.0", @@ -52933,7 +52934,7 @@ }, "packages/blocks": { "name": "@wordpress/blocks", - "version": "13.7.0", + "version": "13.8.5", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -52987,7 +52988,7 @@ }, "packages/browserslist-config": { "name": "@wordpress/browserslist-config", - "version": "6.7.0", + "version": "6.8.1", "dev": true, "license": "GPL-2.0-or-later", "engines": { @@ -52997,7 +52998,7 @@ }, "packages/commands": { "name": "@wordpress/commands", - "version": "1.7.0", + "version": "1.8.10", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -53170,7 +53171,7 @@ }, "packages/components": { "name": "@wordpress/components", - "version": "28.7.0", + "version": "28.8.10", "license": "GPL-2.0-or-later", "dependencies": { "@ariakit/react": "^0.4.10", @@ -53290,7 +53291,7 @@ }, "packages/compose": { "name": "@wordpress/compose", - "version": "7.7.0", + "version": "7.8.3", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -53327,7 +53328,7 @@ }, "packages/core-commands": { "name": "@wordpress/core-commands", - "version": "1.7.0", + "version": "1.8.14", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -53340,6 +53341,7 @@ "@wordpress/html-entities": "file:../html-entities", "@wordpress/i18n": "file:../i18n", "@wordpress/icons": "file:../icons", + "@wordpress/notices": "file:../notices", "@wordpress/private-apis": "file:../private-apis", "@wordpress/router": "file:../router", "@wordpress/url": "file:../url" @@ -53355,7 +53357,7 @@ }, "packages/core-data": { "name": "@wordpress/core-data", - "version": "7.7.0", + "version": "7.8.14", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -53400,7 +53402,7 @@ }, "packages/create-block": { "name": "@wordpress/create-block", - "version": "4.50.0", + "version": "4.51.1", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -53428,7 +53430,7 @@ }, "packages/create-block-tutorial-template": { "name": "@wordpress/create-block-tutorial-template", - "version": "4.7.0", + "version": "4.8.1", "dev": true, "license": "GPL-2.0-or-later", "engines": { @@ -53438,7 +53440,7 @@ }, "packages/customize-widgets": { "name": "@wordpress/customize-widgets", - "version": "5.7.0", + "version": "5.8.15", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -53476,7 +53478,7 @@ }, "packages/data": { "name": "@wordpress/data", - "version": "10.7.0", + "version": "10.8.3", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -53505,7 +53507,7 @@ }, "packages/data-controls": { "name": "@wordpress/data-controls", - "version": "4.7.0", + "version": "4.8.3", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -53523,7 +53525,7 @@ }, "packages/dataviews": { "name": "@wordpress/dataviews", - "version": "4.3.0", + "version": "4.4.10", "license": "GPL-2.0-or-later", "dependencies": { "@ariakit/react": "^0.4.10", @@ -53580,7 +53582,7 @@ }, "packages/date": { "name": "@wordpress/date", - "version": "5.7.0", + "version": "5.8.2", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -53595,7 +53597,7 @@ }, "packages/dependency-extraction-webpack-plugin": { "name": "@wordpress/dependency-extraction-webpack-plugin", - "version": "6.7.0", + "version": "6.8.3", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -53611,7 +53613,7 @@ }, "packages/deprecated": { "name": "@wordpress/deprecated", - "version": "4.7.0", + "version": "4.8.2", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -53624,7 +53626,7 @@ }, "packages/docgen": { "name": "@wordpress/docgen", - "version": "2.7.0", + "version": "2.8.1", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -53646,7 +53648,7 @@ }, "packages/dom": { "name": "@wordpress/dom", - "version": "4.7.0", + "version": "4.8.2", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -53659,7 +53661,7 @@ }, "packages/dom-ready": { "name": "@wordpress/dom-ready", - "version": "4.7.0", + "version": "4.8.1", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0" @@ -53671,7 +53673,7 @@ }, "packages/e2e-test-utils": { "name": "@wordpress/e2e-test-utils", - "version": "11.7.0", + "version": "11.8.2", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -53694,7 +53696,7 @@ }, "packages/e2e-test-utils-playwright": { "name": "@wordpress/e2e-test-utils-playwright", - "version": "1.7.0", + "version": "1.8.1", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -53722,7 +53724,7 @@ }, "packages/e2e-tests": { "name": "@wordpress/e2e-tests", - "version": "8.7.0", + "version": "8.8.8", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -53762,7 +53764,7 @@ }, "packages/edit-post": { "name": "@wordpress/edit-post", - "version": "8.7.0", + "version": "8.8.17", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -53809,7 +53811,7 @@ }, "packages/edit-site": { "name": "@wordpress/edit-site", - "version": "6.7.0", + "version": "6.8.17", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -53871,7 +53873,7 @@ }, "packages/edit-widgets": { "name": "@wordpress/edit-widgets", - "version": "6.7.0", + "version": "6.8.15", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -53914,7 +53916,7 @@ }, "packages/editor": { "name": "@wordpress/editor", - "version": "14.7.0", + "version": "14.8.17", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -53976,7 +53978,7 @@ }, "packages/element": { "name": "@wordpress/element", - "version": "6.7.0", + "version": "6.8.1", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -53995,7 +53997,7 @@ }, "packages/env": { "name": "@wordpress/env", - "version": "10.7.0", + "version": "10.8.1", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -54137,7 +54139,7 @@ }, "packages/escape-html": { "name": "@wordpress/escape-html", - "version": "3.7.0", + "version": "3.8.1", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0" @@ -54149,7 +54151,7 @@ }, "packages/eslint-plugin": { "name": "@wordpress/eslint-plugin", - "version": "21.0.0", + "version": "21.1.2", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -54192,7 +54194,7 @@ }, "packages/fields": { "name": "@wordpress/fields", - "version": "0.0.1", + "version": "0.0.15", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54227,7 +54229,7 @@ }, "packages/format-library": { "name": "@wordpress/format-library", - "version": "5.7.0", + "version": "5.8.14", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54255,7 +54257,7 @@ }, "packages/hooks": { "name": "@wordpress/hooks", - "version": "4.7.0", + "version": "4.8.2", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0" @@ -54267,7 +54269,7 @@ }, "packages/html-entities": { "name": "@wordpress/html-entities", - "version": "4.7.0", + "version": "4.8.1", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0" @@ -54279,7 +54281,7 @@ }, "packages/i18n": { "name": "@wordpress/i18n", - "version": "5.7.0", + "version": "5.8.2", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54299,7 +54301,7 @@ }, "packages/icons": { "name": "@wordpress/icons", - "version": "10.7.0", + "version": "10.8.2", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54313,7 +54315,7 @@ }, "packages/interactivity": { "name": "@wordpress/interactivity", - "version": "6.7.0", + "version": "6.8.5", "license": "GPL-2.0-or-later", "dependencies": { "@preact/signals": "^1.2.2", @@ -54326,7 +54328,7 @@ }, "packages/interactivity-router": { "name": "@wordpress/interactivity-router", - "version": "2.7.0", + "version": "2.8.6", "license": "GPL-2.0-or-later", "dependencies": { "@wordpress/a11y": "file:../a11y", @@ -54363,7 +54365,7 @@ }, "packages/interface": { "name": "@wordpress/interface", - "version": "6.7.0", + "version": "6.8.10", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54392,7 +54394,7 @@ }, "packages/is-shallow-equal": { "name": "@wordpress/is-shallow-equal", - "version": "5.7.0", + "version": "5.8.1", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0" @@ -54404,7 +54406,7 @@ }, "packages/jest-console": { "name": "@wordpress/jest-console", - "version": "8.7.0", + "version": "8.8.1", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -54421,7 +54423,7 @@ }, "packages/jest-preset-default": { "name": "@wordpress/jest-preset-default", - "version": "12.7.0", + "version": "12.8.1", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -54439,7 +54441,7 @@ }, "packages/jest-puppeteer-axe": { "name": "@wordpress/jest-puppeteer-axe", - "version": "7.7.0", + "version": "7.8.1", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -54462,7 +54464,7 @@ }, "packages/keyboard-shortcuts": { "name": "@wordpress/keyboard-shortcuts", - "version": "5.7.0", + "version": "5.8.3", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54480,7 +54482,7 @@ }, "packages/keycodes": { "name": "@wordpress/keycodes", - "version": "4.7.0", + "version": "4.8.2", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54493,7 +54495,7 @@ }, "packages/lazy-import": { "name": "@wordpress/lazy-import", - "version": "2.7.0", + "version": "2.8.1", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -54508,7 +54510,7 @@ }, "packages/list-reusable-blocks": { "name": "@wordpress/list-reusable-blocks", - "version": "5.7.0", + "version": "5.8.10", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54531,7 +54533,7 @@ }, "packages/media-utils": { "name": "@wordpress/media-utils", - "version": "5.7.0", + "version": "5.8.2", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54547,7 +54549,7 @@ }, "packages/notices": { "name": "@wordpress/notices", - "version": "5.7.0", + "version": "5.8.3", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54564,7 +54566,7 @@ }, "packages/npm-package-json-lint-config": { "name": "@wordpress/npm-package-json-lint-config", - "version": "5.7.0", + "version": "5.8.1", "dev": true, "license": "GPL-2.0-or-later", "engines": { @@ -54577,7 +54579,7 @@ }, "packages/nux": { "name": "@wordpress/nux", - "version": "9.7.0", + "version": "9.8.10", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54600,7 +54602,7 @@ }, "packages/patterns": { "name": "@wordpress/patterns", - "version": "2.7.0", + "version": "2.8.14", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54630,7 +54632,7 @@ }, "packages/plugins": { "name": "@wordpress/plugins", - "version": "7.7.0", + "version": "7.8.10", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54653,7 +54655,7 @@ }, "packages/postcss-plugins-preset": { "name": "@wordpress/postcss-plugins-preset", - "version": "5.7.0", + "version": "5.8.3", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -54670,7 +54672,7 @@ }, "packages/postcss-themes": { "name": "@wordpress/postcss-themes", - "version": "6.7.0", + "version": "6.8.1", "dev": true, "license": "GPL-2.0-or-later", "engines": { @@ -54683,7 +54685,7 @@ }, "packages/preferences": { "name": "@wordpress/preferences", - "version": "4.7.0", + "version": "4.8.10", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54709,7 +54711,7 @@ }, "packages/preferences-persistence": { "name": "@wordpress/preferences-persistence", - "version": "2.7.0", + "version": "2.8.2", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54722,7 +54724,7 @@ }, "packages/prettier-config": { "name": "@wordpress/prettier-config", - "version": "4.7.0", + "version": "4.8.1", "dev": true, "license": "GPL-2.0-or-later", "engines": { @@ -54735,7 +54737,7 @@ }, "packages/primitives": { "name": "@wordpress/primitives", - "version": "4.7.0", + "version": "4.8.1", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54752,7 +54754,7 @@ }, "packages/priority-queue": { "name": "@wordpress/priority-queue", - "version": "3.7.0", + "version": "3.8.1", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54765,7 +54767,7 @@ }, "packages/private-apis": { "name": "@wordpress/private-apis", - "version": "1.7.0", + "version": "1.8.1", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0" @@ -54777,7 +54779,7 @@ }, "packages/project-management-automation": { "name": "@wordpress/project-management-automation", - "version": "2.7.0", + "version": "2.8.1", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -54807,7 +54809,7 @@ }, "packages/react-i18n": { "name": "@wordpress/react-i18n", - "version": "4.7.0", + "version": "4.8.2", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54963,7 +54965,7 @@ }, "packages/readable-js-assets-webpack-plugin": { "name": "@wordpress/readable-js-assets-webpack-plugin", - "version": "3.7.0", + "version": "3.8.1", "dev": true, "license": "GPL-2.0-or-later", "engines": { @@ -54976,7 +54978,7 @@ }, "packages/redux-routine": { "name": "@wordpress/redux-routine", - "version": "5.7.0", + "version": "5.8.1", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55021,7 +55023,7 @@ }, "packages/reusable-blocks": { "name": "@wordpress/reusable-blocks", - "version": "5.7.0", + "version": "5.8.14", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55048,7 +55050,7 @@ }, "packages/rich-text": { "name": "@wordpress/rich-text", - "version": "7.7.0", + "version": "7.8.3", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55072,7 +55074,7 @@ }, "packages/router": { "name": "@wordpress/router", - "version": "1.7.0", + "version": "1.8.1", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55091,7 +55093,7 @@ }, "packages/scripts": { "name": "@wordpress/scripts", - "version": "29.0.0", + "version": "30.0.6", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -55546,7 +55548,7 @@ }, "packages/server-side-render": { "name": "@wordpress/server-side-render", - "version": "5.7.0", + "version": "5.8.10", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55572,7 +55574,7 @@ }, "packages/shortcode": { "name": "@wordpress/shortcode", - "version": "4.7.0", + "version": "4.8.1", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55585,7 +55587,7 @@ }, "packages/style-engine": { "name": "@wordpress/style-engine", - "version": "2.7.0", + "version": "2.8.1", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55598,7 +55600,7 @@ }, "packages/stylelint-config": { "name": "@wordpress/stylelint-config", - "version": "22.7.0", + "version": "23.0.1", "dev": true, "license": "MIT", "dependencies": { @@ -55716,7 +55718,7 @@ }, "packages/sync": { "name": "@wordpress/sync", - "version": "1.7.0", + "version": "1.8.1", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55737,7 +55739,7 @@ }, "packages/token-list": { "name": "@wordpress/token-list", - "version": "3.7.0", + "version": "3.8.1", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0" @@ -55749,7 +55751,7 @@ }, "packages/undo-manager": { "name": "@wordpress/undo-manager", - "version": "1.7.0", + "version": "1.8.1", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55762,7 +55764,7 @@ }, "packages/url": { "name": "@wordpress/url", - "version": "4.7.0", + "version": "4.8.1", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55775,7 +55777,7 @@ }, "packages/viewport": { "name": "@wordpress/viewport", - "version": "6.7.0", + "version": "6.8.3", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55793,7 +55795,7 @@ }, "packages/warning": { "name": "@wordpress/warning", - "version": "3.7.0", + "version": "3.8.1", "license": "GPL-2.0-or-later", "engines": { "node": ">=18.12.0", @@ -55802,7 +55804,7 @@ }, "packages/widgets": { "name": "@wordpress/widgets", - "version": "4.7.0", + "version": "4.8.14", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55830,7 +55832,7 @@ }, "packages/wordcount": { "name": "@wordpress/wordcount", - "version": "4.7.0", + "version": "4.8.1", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0" @@ -67939,7 +67941,8 @@ "@wordpress/plugins": "file:../plugins", "@wordpress/private-apis": "file:../private-apis", "@wordpress/url": "file:../url", - "change-case": "^4.1.2" + "change-case": "^4.1.2", + "clsx": "^2.1.1" } }, "@wordpress/block-editor": { @@ -68362,6 +68365,7 @@ "@wordpress/html-entities": "file:../html-entities", "@wordpress/i18n": "file:../i18n", "@wordpress/icons": "file:../icons", + "@wordpress/notices": "file:../notices", "@wordpress/private-apis": "file:../private-apis", "@wordpress/router": "file:../router", "@wordpress/url": "file:../url" diff --git a/package.json b/package.json index 46d2b42f267abb..a4cc002adbf8e7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "19.2.0", + "version": "19.3.0-rc.1", "private": true, "description": "A new WordPress editor experience.", "author": "The WordPress Contributors", diff --git a/packages/a11y/CHANGELOG.md b/packages/a11y/CHANGELOG.md index 36973cce020f33..626488313977d1 100644 --- a/packages/a11y/CHANGELOG.md +++ b/packages/a11y/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 4.8.0 (2024-09-19) + ## 4.7.0 (2024-09-05) ## 4.6.0 (2024-08-21) diff --git a/packages/a11y/package.json b/packages/a11y/package.json index 327d6b9ff07167..3331c9eee9318a 100644 --- a/packages/a11y/package.json +++ b/packages/a11y/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/a11y", - "version": "4.7.0", + "version": "4.8.2", "description": "Accessibility (a11y) utilities for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/annotations/CHANGELOG.md b/packages/annotations/CHANGELOG.md index 6211cf30688e5c..7ac3e50c3fd8b6 100644 --- a/packages/annotations/CHANGELOG.md +++ b/packages/annotations/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 3.8.0 (2024-09-19) + ## 3.7.0 (2024-09-05) ## 3.6.0 (2024-08-21) diff --git a/packages/annotations/package.json b/packages/annotations/package.json index c189dcc11fd09f..08dacdc338aa4d 100644 --- a/packages/annotations/package.json +++ b/packages/annotations/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/annotations", - "version": "3.7.0", + "version": "3.8.3", "description": "Annotate content in the Gutenberg editor.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/api-fetch/CHANGELOG.md b/packages/api-fetch/CHANGELOG.md index 01b37d4e14a7d1..7c333170b6cc11 100644 --- a/packages/api-fetch/CHANGELOG.md +++ b/packages/api-fetch/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 7.8.0 (2024-09-19) + ## 7.7.0 (2024-09-05) ## 7.6.0 (2024-08-21) diff --git a/packages/api-fetch/package.json b/packages/api-fetch/package.json index 76d797f4ddfa57..932adab12aeb89 100644 --- a/packages/api-fetch/package.json +++ b/packages/api-fetch/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/api-fetch", - "version": "7.7.0", + "version": "7.8.2", "description": "Utility to make WordPress REST API requests.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/autop/CHANGELOG.md b/packages/autop/CHANGELOG.md index ed19f1ceafbc58..4bb494dcfff5c0 100644 --- a/packages/autop/CHANGELOG.md +++ b/packages/autop/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 4.8.0 (2024-09-19) + ## 4.7.0 (2024-09-05) ## 4.6.0 (2024-08-21) diff --git a/packages/autop/package.json b/packages/autop/package.json index 4e1cf577ebd09c..ca4233a7289fee 100644 --- a/packages/autop/package.json +++ b/packages/autop/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/autop", - "version": "4.7.0", + "version": "4.8.1", "description": "WordPress's automatic paragraph functions `autop` and `removep`.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/babel-plugin-import-jsx-pragma/CHANGELOG.md b/packages/babel-plugin-import-jsx-pragma/CHANGELOG.md index c916b788243a06..5a68cf8eb40400 100644 --- a/packages/babel-plugin-import-jsx-pragma/CHANGELOG.md +++ b/packages/babel-plugin-import-jsx-pragma/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 5.8.0 (2024-09-19) + ## 5.7.0 (2024-09-05) ## 5.6.0 (2024-08-21) diff --git a/packages/babel-plugin-import-jsx-pragma/package.json b/packages/babel-plugin-import-jsx-pragma/package.json index 1af6bdf93cb044..479fd198deba9b 100644 --- a/packages/babel-plugin-import-jsx-pragma/package.json +++ b/packages/babel-plugin-import-jsx-pragma/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/babel-plugin-import-jsx-pragma", - "version": "5.7.0", + "version": "5.8.1", "description": "Babel transform plugin for automatically injecting an import to be used as the pragma for the React JSX Transform plugin.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/babel-plugin-makepot/CHANGELOG.md b/packages/babel-plugin-makepot/CHANGELOG.md index e7345f314d5788..d878e1fb6b020c 100644 --- a/packages/babel-plugin-makepot/CHANGELOG.md +++ b/packages/babel-plugin-makepot/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 6.8.0 (2024-09-19) + ## 6.7.0 (2024-09-05) ## 6.6.0 (2024-08-21) diff --git a/packages/babel-plugin-makepot/package.json b/packages/babel-plugin-makepot/package.json index a7c18d891538d1..ff260388085419 100644 --- a/packages/babel-plugin-makepot/package.json +++ b/packages/babel-plugin-makepot/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/babel-plugin-makepot", - "version": "6.7.0", + "version": "6.8.1", "description": "WordPress Babel internationalization (i18n) plugin.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/babel-preset-default/CHANGELOG.md b/packages/babel-preset-default/CHANGELOG.md index c8c3fdb66ecb0e..b31be6ffd8d56d 100644 --- a/packages/babel-preset-default/CHANGELOG.md +++ b/packages/babel-preset-default/CHANGELOG.md @@ -1,8 +1,16 @@ -## Internal +## Unreleased -- Added `addPolyfillComments` option. When used, it will automatically add magic comments to mark files that need `wp-polyfill`. +### Bug Fixes + +- Fix a bug in 8.8.1 due to missing files in the published package ([#65481](https://github.com/WordPress/gutenberg/pull/65481)). + +## 8.8.0 (2024-09-19) + +### Internal + +- Added `addPolyfillComments` option. When used, it will automatically add magic comments to mark files that need `wp-polyfill` ([#65292](https://github.com/WordPress/gutenberg/pull/65292)). ## 8.7.0 (2024-09-05) diff --git a/packages/babel-preset-default/package.json b/packages/babel-preset-default/package.json index be5b3786346eb2..8c3310fbc03c07 100644 --- a/packages/babel-preset-default/package.json +++ b/packages/babel-preset-default/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/babel-preset-default", - "version": "8.7.0", + "version": "8.8.2", "description": "Default Babel preset for WordPress development.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", @@ -26,7 +26,9 @@ }, "files": [ "build", - "index.js" + "index.js", + "polyfill-exclusions.js", + "replace-polyfills.js" ], "main": "index.js", "dependencies": { diff --git a/packages/base-styles/CHANGELOG.md b/packages/base-styles/CHANGELOG.md index cad2f94c98da7b..ea6b765aea3f11 100644 --- a/packages/base-styles/CHANGELOG.md +++ b/packages/base-styles/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 5.8.0 (2024-09-19) + ## 5.7.0 (2024-09-05) ## 5.6.0 (2024-08-21) diff --git a/packages/base-styles/_animations.scss b/packages/base-styles/_animations.scss index b17478ed24f040..ae5de9a803008c 100644 --- a/packages/base-styles/_animations.scss +++ b/packages/base-styles/_animations.scss @@ -36,13 +36,13 @@ @include reduce-motion("animation"); } -@mixin editor-canvas-resize-animation() { - transition: all 0.5s cubic-bezier(0.65, 0, 0.45, 1); - @include reduce-motion("transition"); -} - // Deprecated @mixin edit-post__fade-in-animation($speed: 0.08s, $delay: 0s) { @warn "The `edit-post__fade-in-animation` mixin is deprecated. Use `animation__fade-in` instead."; @include animation__fade-in($speed, $delay); } + +@mixin editor-canvas-resize-animation($additional-transition-rules...) { + transition: all 400ms cubic-bezier(0.46, 0.03, 0.52, 0.96), $additional-transition-rules; + @include reduce-motion("transition"); +} diff --git a/packages/base-styles/package.json b/packages/base-styles/package.json index 6839a8081e04a8..6e69b8f39121e3 100644 --- a/packages/base-styles/package.json +++ b/packages/base-styles/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/base-styles", - "version": "5.7.0", + "version": "5.8.3", "description": "Base SCSS utilities and variables for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/blob/CHANGELOG.md b/packages/blob/CHANGELOG.md index f95598025a8b68..c13c6e6faf6ac8 100644 --- a/packages/blob/CHANGELOG.md +++ b/packages/blob/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 4.8.0 (2024-09-19) + ## 4.7.0 (2024-09-05) ## 4.6.0 (2024-08-21) diff --git a/packages/blob/package.json b/packages/blob/package.json index 3dea70c9771976..c2408ea8a12a03 100644 --- a/packages/blob/package.json +++ b/packages/blob/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/blob", - "version": "4.7.0", + "version": "4.8.1", "description": "Blob utilities for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/block-directory/CHANGELOG.md b/packages/block-directory/CHANGELOG.md index 70914f293ca998..8c25431f1b5b5e 100644 --- a/packages/block-directory/CHANGELOG.md +++ b/packages/block-directory/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 5.8.0 (2024-09-19) + ## 5.7.0 (2024-09-05) ## 5.6.0 (2024-08-21) diff --git a/packages/block-directory/package.json b/packages/block-directory/package.json index 39f239edfcb44a..0b2a58c55ec2b8 100644 --- a/packages/block-directory/package.json +++ b/packages/block-directory/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/block-directory", - "version": "5.7.0", + "version": "5.8.17", "description": "Extend editor with block directory features to search, download and install blocks.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", @@ -45,7 +45,8 @@ "@wordpress/plugins": "file:../plugins", "@wordpress/private-apis": "file:../private-apis", "@wordpress/url": "file:../url", - "change-case": "^4.1.2" + "change-case": "^4.1.2", + "clsx": "^2.1.1" }, "peerDependencies": { "react": "^18.0.0", diff --git a/packages/block-directory/src/components/downloadable-block-list-item/index.js b/packages/block-directory/src/components/downloadable-block-list-item/index.js index ac587dc2d6d0cc..c04ea8ea1767bd 100644 --- a/packages/block-directory/src/components/downloadable-block-list-item/index.js +++ b/packages/block-directory/src/components/downloadable-block-list-item/index.js @@ -1,9 +1,14 @@ +/** + * External dependencies + */ +import clsx from 'clsx'; + /** * WordPress dependencies */ import { __, _n, sprintf } from '@wordpress/i18n'; import { - Button, + Tooltip, Spinner, VisuallyHidden, Composite, @@ -29,28 +34,28 @@ function getDownloadableBlockLabel( const stars = Math.round( rating / 0.5 ) * 0.5; if ( ! isInstalled && hasNotice ) { - /* translators: %1$s: block title */ + /* translators: %s: block title */ return sprintf( 'Retry installing %s.', decodeEntities( title ) ); } if ( isInstalled ) { - /* translators: %1$s: block title */ + /* translators: %s: block title */ return sprintf( 'Add %s.', decodeEntities( title ) ); } if ( isInstalling ) { - /* translators: %1$s: block title */ + /* translators: %s: block title */ return sprintf( 'Installing %s.', decodeEntities( title ) ); } // No ratings yet, just use the title. if ( ratingCount < 1 ) { - /* translators: %1$s: block title */ + /* translators: %s: block title */ return sprintf( 'Install %s.', decodeEntities( title ) ); } return sprintf( - /* translators: %1$s: block title, %2$s: average rating, %3$s: total ratings count. */ + /* translators: 1: block title, 2: average rating, 3: total ratings count. */ _n( 'Install %1$s. %2$s stars with %3$s review.', 'Install %1$s. %2$s stars with %3$s reviews.', @@ -89,77 +94,75 @@ function DownloadableBlockListItem( { item, onClick } ) { statusText = __( 'Installing…' ); } + const itemLabel = getDownloadableBlockLabel( item, { + hasNotice, + isInstalled, + isInstalling, + } ); + return ( - { - event.preventDefault(); - onClick(); - } } - label={ getDownloadableBlockLabel( item, { - hasNotice, - isInstalled, - isInstalling, - } ) } - showTooltip - tooltipPosition="top center" - /> - } - disabled={ isInstalling || ! isInstallable } - > -
- - { isInstalling ? ( - - - - ) : ( - + + - - - { createInterpolateElement( - sprintf( - /* translators: %1$s: block title, %2$s: author name. */ - __( '%1$s by %2$s' ), - decodeEntities( title ), - author - ), - { - span: ( - + accessibleWhenDisabled + disabled={ isInstalling || ! isInstallable } + onClick={ ( event ) => { + event.preventDefault(); + onClick(); + } } + aria-label={ itemLabel } + type="button" + role="option" + > +
+ + { isInstalling ? ( + + + + ) : ( + + ) } +
+ + + { createInterpolateElement( + sprintf( + /* translators: 1: block title. 2: author name. */ + __( '%1$s by %2$s' ), + decodeEntities( title ), + author ), - } + { + span: ( + + ), + } + ) } + + { hasNotice ? ( + + ) : ( + <> + + { !! statusText + ? statusText + : decodeEntities( description ) } + + { isInstallable && + ! ( isInstalled || isInstalling ) && ( + + { __( 'Install block' ) } + + ) } + ) } - { hasNotice ? ( - - ) : ( - <> - - { !! statusText - ? statusText - : decodeEntities( description ) } - - { isInstallable && - ! ( isInstalled || isInstalling ) && ( - - { __( 'Install block' ) } - - ) } - - ) } - -
+ +
); } diff --git a/packages/block-directory/src/components/downloadable-block-list-item/style.scss b/packages/block-directory/src/components/downloadable-block-list-item/style.scss index 8f95297bd9ef0d..6fce5e1b5b32a7 100644 --- a/packages/block-directory/src/components/downloadable-block-list-item/style.scss +++ b/packages/block-directory/src/components/downloadable-block-list-item/style.scss @@ -1,10 +1,21 @@ .block-directory-downloadable-block-list-item { - padding: $grid-unit-15; + & + & { + margin-top: $grid-unit-05; + } + + display: grid; + grid-template-columns: auto 1fr; + width: 100%; height: auto; + padding: $grid-unit-15; + margin: 0; + + appearance: none; + background: none; + border: 0; text-align: left; - display: grid; - grid-template-columns: auto 1fr; + transition: box-shadow 0.1s linear; // The item contains absolutely positioned items. // Set `position: relative` on the parent to prevent overflow issues @@ -12,13 +23,20 @@ // See: https://github.com/WordPress/gutenberg/issues/63384 position: relative; + + &:not([aria-disabled="true"]) { + cursor: pointer; + } + &:hover { @include button-style__focus(); } - &.is-busy { - background: transparent; + &[data-focus-visible] { + @include button-style__focus(); + } + &.is-installing { .block-directory-downloadable-block-list-item__author { border: 0; clip: rect(1px, 1px, 1px, 1px); @@ -33,11 +51,6 @@ word-wrap: normal !important; } } - - &:disabled, - &[aria-disabled] { - opacity: 1; - } } .block-directory-downloadable-block-list-item__icon { @@ -56,6 +69,11 @@ align-items: center; justify-content: center; } + + .is-installing & { + // Adding an extra 6px to avoid the UI from jumping when the rating bar gets hidden + margin-right: $grid-unit-20 + 6px; + } } .block-directory-block-ratings { diff --git a/packages/block-directory/src/components/downloadable-blocks-panel/style.scss b/packages/block-directory/src/components/downloadable-blocks-panel/style.scss index ff3fdb9ea8e319..f4df5ad4abda53 100644 --- a/packages/block-directory/src/components/downloadable-blocks-panel/style.scss +++ b/packages/block-directory/src/components/downloadable-blocks-panel/style.scss @@ -32,6 +32,3 @@ margin-top: 0; } -.block-directory-downloadable-blocks-panel button { - margin-top: $grid-unit-05; -} diff --git a/packages/block-directory/src/plugins/get-install-missing/index.js b/packages/block-directory/src/plugins/get-install-missing/index.js index 8b192cbe8fdc49..43c051cb9aa371 100644 --- a/packages/block-directory/src/plugins/get-install-missing/index.js +++ b/packages/block-directory/src/plugins/get-install-missing/index.js @@ -101,8 +101,7 @@ const ModifiedWarning = ( { originalBlock, ...props } ) => { ); actions.push( - } - /> + + + { isActive && } + + } + /> + ); } @@ -143,11 +142,7 @@ function renderShadowToggle() { }; return ( - - } - /> - ) ) } + + + + ) + ) } diff --git a/packages/block-editor/src/components/inspector-popover-header/index.js b/packages/block-editor/src/components/inspector-popover-header/index.js index d543ab0298cc62..cf6bf0d3d6796e 100644 --- a/packages/block-editor/src/components/inspector-popover-header/index.js +++ b/packages/block-editor/src/components/inspector-popover-header/index.js @@ -31,8 +31,7 @@ export default function InspectorPopoverHeader( { { actions.map( ( { label, icon, onClick } ) => ( + ); } diff --git a/packages/block-editor/src/components/list-view/style.scss b/packages/block-editor/src/components/list-view/style.scss index 05a04abfd110b4..85ae22dfb51a74 100644 --- a/packages/block-editor/src/components/list-view/style.scss +++ b/packages/block-editor/src/components/list-view/style.scss @@ -41,6 +41,15 @@ &:hover { color: var(--wp-admin-theme-color); } + + svg { + fill: currentColor; + // Optimizate for high contrast modes. + // See also https://blogs.windows.com/msedgedev/2020/09/17/styling-for-windows-high-contrast-with-new-standards-for-forced-colors/. + @media (forced-colors: active) { + fill: CanvasText; + } + } } &:not(.is-selected) .block-editor-list-view-block-select-button { @@ -216,6 +225,15 @@ text-align: left; position: relative; white-space: nowrap; + border-radius: 2px; + box-sizing: border-box; + color: inherit; + font-family: inherit; + font-size: 13px; + font-weight: 400; + margin: 0; + text-decoration: none; + transition: box-shadow 0.1s linear; &.is-dropping-before::before { content: ""; diff --git a/packages/block-editor/src/components/media-placeholder/content.scss b/packages/block-editor/src/components/media-placeholder/content.scss index eeb2928df80baf..2f7bb2e673f12e 100644 --- a/packages/block-editor/src/components/media-placeholder/content.scss +++ b/packages/block-editor/src/components/media-placeholder/content.scss @@ -1,27 +1,11 @@ .block-editor-media-placeholder__url-input-form { - display: flex; - - // Selector requires a lot of specificity to override base styles. - input[type="url"].block-editor-media-placeholder__url-input-field { - width: 100%; - min-width: 200px; - - @include break-small() { - width: 300px; - } - - flex-grow: 1; - border: none; - border-radius: 0; - margin: 2px; + min-width: 260px; + @include break-small() { + width: 300px; } } -.block-editor-media-placeholder__url-input-submit-button { - flex-shrink: 1; -} - .block-editor-media-placeholder__cancel-button.is-link { margin: 1em; display: block; diff --git a/packages/block-editor/src/components/media-placeholder/index.js b/packages/block-editor/src/components/media-placeholder/index.js index 4d41289f324c0f..f16e4317227235 100644 --- a/packages/block-editor/src/components/media-placeholder/index.js +++ b/packages/block-editor/src/components/media-placeholder/index.js @@ -11,6 +11,8 @@ import { FormFileUpload, Placeholder, DropZone, + __experimentalInputControl as InputControl, + __experimentalInputControlSuffixWrapper as InputControlSuffixWrapper, withFilters, } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; @@ -42,21 +44,23 @@ const InsertFromURLPopover = ( { className="block-editor-media-placeholder__url-input-form" onSubmit={ onSubmit } > - -
diff --git a/packages/block-editor/src/components/url-input/index.js b/packages/block-editor/src/components/url-input/index.js index 25c033e88749bb..4e7ac61e783661 100644 --- a/packages/block-editor/src/components/url-input/index.js +++ b/packages/block-editor/src/components/url-input/index.js @@ -196,7 +196,7 @@ class URLInput extends Component { if ( !! suggestions.length ) { this.props.debouncedSpeak( sprintf( - /* translators: %s: number of results. */ + /* translators: %d: number of results. */ _n( '%d result found, use up and down arrow keys to navigate.', '%d results found, use up and down arrow keys to navigate.', @@ -540,8 +540,7 @@ class URLInput extends Component { > { suggestions.map( ( suggestion, index ) => ( + value={ name } + label={ label } + /> ); } ) } - + ); } diff --git a/packages/block-editor/src/hooks/use-bindings-attributes.js b/packages/block-editor/src/hooks/use-bindings-attributes.js index e1ebf5fda6b8ee..fdc617fda20c05 100644 --- a/packages/block-editor/src/hooks/use-bindings-attributes.js +++ b/packages/block-editor/src/hooks/use-bindings-attributes.js @@ -103,11 +103,7 @@ export const withBlockBindingSupport = createHigherOrderComponent( const sources = useSelect( ( select ) => unlock( select( blocksStore ) ).getAllBlockBindingsSources() ); - const { name, clientId } = props; - const hasParentPattern = !! props.context[ 'pattern/overrides' ]; - const hasPatternOverridesDefaultBinding = - props.attributes.metadata?.bindings?.[ DEFAULT_ATTRIBUTE ] - ?.source === 'core/pattern-overrides'; + const { name, clientId, context, setAttributes } = props; const blockBindings = useMemo( () => replacePatternOverrideDefaultBindings( @@ -121,79 +117,87 @@ export const withBlockBindingSupport = createHigherOrderComponent( // used purposely here to ensure `boundAttributes` is updated whenever // there are attribute updates. // `source.getValues` may also call a selector via `registry.select`. - const boundAttributes = useSelect( () => { - if ( ! blockBindings ) { - return; - } - - const attributes = {}; - - const blockBindingsBySource = new Map(); - - for ( const [ attributeName, binding ] of Object.entries( - blockBindings - ) ) { - const { source: sourceName, args: sourceArgs } = binding; - const source = sources[ sourceName ]; - if ( ! source || ! canBindAttribute( name, attributeName ) ) { - continue; + const updatedContext = {}; + const boundAttributes = useSelect( + ( select ) => { + if ( ! blockBindings ) { + return; } - blockBindingsBySource.set( source, { - ...blockBindingsBySource.get( source ), - [ attributeName ]: { - args: sourceArgs, - }, - } ); - } + const attributes = {}; - if ( blockBindingsBySource.size ) { - for ( const [ source, bindings ] of blockBindingsBySource ) { - // Populate context. - const context = {}; + const blockBindingsBySource = new Map(); - if ( source.usesContext?.length ) { - for ( const key of source.usesContext ) { - context[ key ] = blockContext[ key ]; - } + for ( const [ attributeName, binding ] of Object.entries( + blockBindings + ) ) { + const { source: sourceName, args: sourceArgs } = binding; + const source = sources[ sourceName ]; + if ( + ! source || + ! canBindAttribute( name, attributeName ) + ) { + continue; } - // Get values in batch if the source supports it. - let values = {}; - if ( ! source.getValues ) { - Object.keys( bindings ).forEach( ( attr ) => { - // Default to the `key` or the source label when `getValues` doesn't exist - values[ attr ] = - bindings[ attr ].args?.key || source.label; - } ); - } else { - values = source.getValues( { - registry, - context, - clientId, - bindings, - } ); + // Populate context. + for ( const key of source.usesContext || [] ) { + updatedContext[ key ] = blockContext[ key ]; } - for ( const [ attributeName, value ] of Object.entries( - values - ) ) { - if ( - attributeName === 'url' && - ( ! value || ! isURLLike( value ) ) - ) { - // Return null if value is not a valid URL. - attributes[ attributeName ] = null; + + blockBindingsBySource.set( source, { + ...blockBindingsBySource.get( source ), + [ attributeName ]: { + args: sourceArgs, + }, + } ); + } + + if ( blockBindingsBySource.size ) { + for ( const [ + source, + bindings, + ] of blockBindingsBySource ) { + // Get values in batch if the source supports it. + let values = {}; + if ( ! source.getValues ) { + Object.keys( bindings ).forEach( ( attr ) => { + // Default to the the source label when `getValues` doesn't exist. + values[ attr ] = source.label; + } ); } else { - attributes[ attributeName ] = value; + values = source.getValues( { + select, + context: updatedContext, + clientId, + bindings, + } ); + } + for ( const [ attributeName, value ] of Object.entries( + values + ) ) { + if ( + attributeName === 'url' && + ( ! value || ! isURLLike( value ) ) + ) { + // Return null if value is not a valid URL. + attributes[ attributeName ] = null; + } else { + attributes[ attributeName ] = value; + } } } } - } - return attributes; - }, [ blockBindings, name, clientId, blockContext, registry, sources ] ); + return attributes; + }, + [ blockBindings, name, clientId, updatedContext, sources ] + ); - const { setAttributes } = props; + const hasParentPattern = !! updatedContext[ 'pattern/overrides' ]; + const hasPatternOverridesDefaultBinding = + props.attributes.metadata?.bindings?.[ DEFAULT_ATTRIBUTE ] + ?.source === 'core/pattern-overrides'; const _setAttributes = useCallback( ( nextAttributes ) => { @@ -237,18 +241,10 @@ export const withBlockBindingSupport = createHigherOrderComponent( source, bindings, ] of blockBindingsBySource ) { - // Populate context. - const context = {}; - - if ( source.usesContext?.length ) { - for ( const key of source.usesContext ) { - context[ key ] = blockContext[ key ]; - } - } - source.setValues( { - registry, - context, + select: registry.select, + dispatch: registry.dispatch, + context: updatedContext, clientId, bindings, } ); @@ -278,7 +274,7 @@ export const withBlockBindingSupport = createHigherOrderComponent( blockBindings, name, clientId, - blockContext, + updatedContext, setAttributes, sources, hasPatternOverridesDefaultBinding, @@ -292,6 +288,7 @@ export const withBlockBindingSupport = createHigherOrderComponent( { ...props } attributes={ { ...props.attributes, ...boundAttributes } } setAttributes={ _setAttributes } + context={ { ...context, ...updatedContext } } /> ); diff --git a/packages/block-editor/src/hooks/use-zoom-out.js b/packages/block-editor/src/hooks/use-zoom-out.js index d7e21ec1be0578..23511487a54bf7 100644 --- a/packages/block-editor/src/hooks/use-zoom-out.js +++ b/packages/block-editor/src/hooks/use-zoom-out.js @@ -8,15 +8,17 @@ import { useEffect, useRef } from '@wordpress/element'; * Internal dependencies */ import { store as blockEditorStore } from '../store'; - +import { unlock } from '../lock-unlock'; /** * A hook used to set the editor mode to zoomed out mode, invoking the hook sets the mode. * * @param {boolean} zoomOut If we should enter into zoomOut mode or not */ export function useZoomOut( zoomOut = true ) { - const { __unstableSetEditorMode } = useDispatch( blockEditorStore ); - const { __unstableGetEditorMode } = useSelect( blockEditorStore ); + const { __unstableSetEditorMode, setZoomLevel } = unlock( + useDispatch( blockEditorStore ) + ); + const { __unstableGetEditorMode } = unlock( useSelect( blockEditorStore ) ); const originalEditingModeRef = useRef( null ); const mode = __unstableGetEditorMode(); @@ -34,6 +36,7 @@ export function useZoomOut( zoomOut = true ) { __unstableGetEditorMode() !== originalEditingModeRef.current ) { __unstableSetEditorMode( originalEditingModeRef.current ); + setZoomLevel( 100 ); } }; }, [] ); @@ -42,12 +45,19 @@ export function useZoomOut( zoomOut = true ) { useEffect( () => { if ( zoomOut && mode !== 'zoom-out' ) { __unstableSetEditorMode( 'zoom-out' ); + setZoomLevel( 50 ); } else if ( ! zoomOut && __unstableGetEditorMode() === 'zoom-out' && originalEditingModeRef.current !== mode ) { __unstableSetEditorMode( originalEditingModeRef.current ); + setZoomLevel( 100 ); } - }, [ __unstableGetEditorMode, __unstableSetEditorMode, zoomOut ] ); // Mode is deliberately excluded from the dependencies so that the effect does not run when mode changes. + }, [ + __unstableGetEditorMode, + __unstableSetEditorMode, + zoomOut, + setZoomLevel, + ] ); // Mode is deliberately excluded from the dependencies so that the effect does not run when mode changes. } diff --git a/packages/block-editor/src/index.js b/packages/block-editor/src/index.js index db1bc0a06ec6d1..d81f23a702b04e 100644 --- a/packages/block-editor/src/index.js +++ b/packages/block-editor/src/index.js @@ -13,7 +13,6 @@ export { getGapCSSValue as __experimentalGetGapCSSValue, getShadowClassesAndStyles as __experimentalGetShadowClassesAndStyles, useCachedTruthy, - useZoomOut, useStyleOverride, } from './hooks'; export * from './components'; diff --git a/packages/block-editor/src/layouts/flex.js b/packages/block-editor/src/layouts/flex.js index dc7e9d1a167a19..81718449695651 100644 --- a/packages/block-editor/src/layouts/flex.js +++ b/packages/block-editor/src/layouts/flex.js @@ -12,7 +12,6 @@ import { arrowDown, } from '@wordpress/icons'; import { - Button, ToggleControl, Flex, FlexItem, @@ -110,7 +109,6 @@ export default { ) } @@ -190,11 +188,7 @@ export default { }, }; -function FlexLayoutVerticalAlignmentControl( { - layout, - onChange, - isToolbar = false, -} ) { +function FlexLayoutVerticalAlignmentControl( { layout, onChange } ) { const { orientation = 'horizontal' } = layout; const defaultVerticalAlignment = @@ -210,54 +204,17 @@ function FlexLayoutVerticalAlignmentControl( { verticalAlignment: value, } ); }; - if ( isToolbar ) { - return ( - - ); - } - - const verticalAlignmentOptions = [ - { - value: 'flex-start', - label: __( 'Align items top' ), - }, - { - value: 'center', - label: __( 'Align items center' ), - }, - { - value: 'flex-end', - label: __( 'Align items bottom' ), - }, - ]; return ( -
- { __( 'Vertical alignment' ) } -
- { verticalAlignmentOptions.map( ( value, icon, label ) => { - return ( -
-
+ ); } diff --git a/packages/block-editor/src/private-apis.js b/packages/block-editor/src/private-apis.js index 12f477a95a196b..75a9f9144950b4 100644 --- a/packages/block-editor/src/private-apis.js +++ b/packages/block-editor/src/private-apis.js @@ -24,6 +24,7 @@ import { useLayoutClasses, useLayoutStyles, __unstableBlockStyleVariationOverridesWithConfig, + useZoomOut, } from './hooks'; import DimensionsTool from './components/dimensions-tool'; import ResolutionTool from './components/resolution-tool'; @@ -32,6 +33,7 @@ import { usesContextKey } from './components/rich-text/format-edit'; import { ExperimentalBlockCanvas } from './components/block-canvas'; import { getDuotoneFilter } from './components/duotone/utils'; import { useFlashEditableBlocks } from './components/use-flash-editable-blocks'; +import { useZoomOutModeExit } from './components/block-list/use-block-props/use-zoom-out-mode-exit'; import { selectBlockPatternsKey, reusableBlocksSelectKey, @@ -47,7 +49,6 @@ import { PrivatePublishDateTimePicker } from './components/publish-date-time-pic import useSpacingSizes from './components/spacing-sizes-control/hooks/use-spacing-sizes'; import useBlockDisplayTitle from './components/block-title/use-block-display-title'; import TabbedSidebar from './components/tabbed-sidebar'; -import { useBlockBindingsUtils } from './utils/block-bindings'; /** * Private @wordpress/block-editor APIs. @@ -79,6 +80,8 @@ lock( privateApis, { TextAlignmentControl, usesContextKey, useFlashEditableBlocks, + useZoomOutModeExit, + useZoomOut, globalStylesDataKey, globalStylesLinksDataKey, selectBlockPatternsKey, @@ -92,6 +95,5 @@ lock( privateApis, { useBlockDisplayTitle, __unstableBlockStyleVariationOverridesWithConfig, setBackgroundStyleDefaults, - useBlockBindingsUtils, sectionRootClientIdKey, } ); diff --git a/packages/block-editor/src/store/private-actions.js b/packages/block-editor/src/store/private-actions.js index dc57d61fd6b76c..441a07202c42ac 100644 --- a/packages/block-editor/src/store/private-actions.js +++ b/packages/block-editor/src/store/private-actions.js @@ -383,3 +383,26 @@ export const modifyContentLockBlock = focusModeToRevert ); }; + +/** + * Sets the zoom level. + * + * @param {number} zoom the new zoom level + * @return {Object} Action object. + */ +export function setZoomLevel( zoom = 100 ) { + return { + type: 'SET_ZOOM_LEVEL', + zoom, + }; +} + +/** + * Resets the Zoom state. + * @return {Object} Action object. + */ +export function resetZoomLevel() { + return { + type: 'RESET_ZOOM_LEVEL', + }; +} diff --git a/packages/block-editor/src/store/private-selectors.js b/packages/block-editor/src/store/private-selectors.js index 01ad8f69febc9e..42345a1e9db245 100644 --- a/packages/block-editor/src/store/private-selectors.js +++ b/packages/block-editor/src/store/private-selectors.js @@ -15,6 +15,8 @@ import { getBlockName, getTemplateLock, getClientIdsWithDescendants, + isNavigationMode, + __unstableGetEditorMode, } from './selectors'; import { checkAllowListRecursive, @@ -545,7 +547,7 @@ export const getBlockStyles = createSelector( * @return {boolean} Is zoom out mode enabled. */ export function isZoomOutMode( state ) { - return state.editorMode === 'zoom-out'; + return __unstableGetEditorMode( state ) === 'zoom-out'; } /** @@ -560,3 +562,60 @@ export function isZoomOutMode( state ) { export function getSectionRootClientId( state ) { return state.settings?.[ sectionRootClientIdKey ]; } + +/** + * Returns the zoom out state. + * + * @param {Object} state Global application state. + * @return {boolean} The zoom out state. + */ +export function getZoomLevel( state ) { + return state.zoomLevel; +} + +/** + * Returns whether the editor is considered zoomed out. + * + * @param {Object} state Global application state. + * @return {boolean} Whether the editor is zoomed. + */ +export function isZoomOut( state ) { + return getZoomLevel( state ) < 100; +} + +/** + * Retrieves the client ID of the parent section block. + * + * @param {Object} state Global application state. + * @param {string} clientId Client Id of the block. + * + * @return {?string} Client ID of the ancestor block that is content locking the block. + */ +export const getParentSectionBlock = ( state, clientId ) => { + let current = clientId; + let result; + while ( ! result && ( current = state.blocks.parents.get( current ) ) ) { + if ( isSectionBlock( state, current ) ) { + result = current; + } + } + return result; +}; + +/** + * Retrieves the client ID is a content locking parent + * + * @param {Object} state Global application state. + * @param {string} clientId Client Id of the block. + * + * @return {boolean} Whether the block is a content locking parent. + */ +export function isSectionBlock( state, clientId ) { + const sectionRootClientId = getSectionRootClientId( state ); + const sectionClientIds = getBlockOrder( state, sectionRootClientId ); + return ( + getBlockName( state, clientId ) === 'core/block' || + getTemplateLock( state, clientId ) === 'contentOnly' || + ( isNavigationMode( state ) && sectionClientIds.includes( clientId ) ) + ); +} diff --git a/packages/block-editor/src/store/reducer.js b/packages/block-editor/src/store/reducer.js index cd4569c45e5801..e80c90ffb41f27 100644 --- a/packages/block-editor/src/store/reducer.js +++ b/packages/block-editor/src/store/reducer.js @@ -2085,6 +2085,25 @@ export function hoveredBlockClientId( state = false, action ) { return state; } +/** + * Reducer setting zoom out state. + * + * @param {boolean} state Current state. + * @param {Object} action Dispatched action. + * + * @return {boolean} Updated state. + */ +export function zoomLevel( state = 100, action ) { + switch ( action.type ) { + case 'SET_ZOOM_LEVEL': + return action.zoom; + case 'RESET_ZOOM_LEVEL': + return 100; + } + + return state; +} + const combinedReducers = combineReducers( { blocks, isDragging, @@ -2118,6 +2137,7 @@ const combinedReducers = combineReducers( { openedBlockSettingsMenu, registeredInserterMediaCategories, hoveredBlockClientId, + zoomLevel, } ); function withAutomaticChangeReset( reducer ) { diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index 30fdb76bdbe787..c34d96ecf53b85 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -2363,6 +2363,21 @@ const getAllowedPatternsDependants = ( select ) => ( state, rootClientId ) => [ ...getInsertBlockTypeDependants( state, rootClientId ), ]; +const patternsWithParsedBlocks = new WeakMap(); +function enhancePatternWithParsedBlocks( pattern ) { + let enhancedPattern = patternsWithParsedBlocks.get( pattern ); + if ( ! enhancedPattern ) { + enhancedPattern = { + ...pattern, + get blocks() { + return getParsedPattern( pattern ).blocks; + }, + }; + patternsWithParsedBlocks.set( pattern, enhancedPattern ); + } + return enhancedPattern; +} + /** * Returns the list of allowed patterns for inner blocks children. * @@ -2379,14 +2394,7 @@ export const __experimentalGetAllowedPatterns = createRegistrySelector( const { allowedBlockTypes } = getSettings( state ); const parsedPatterns = patterns .filter( ( { inserter = true } ) => !! inserter ) - .map( ( pattern ) => { - return { - ...pattern, - get blocks() { - return getParsedPattern( pattern ).blocks; - }, - }; - } ); + .map( enhancePatternWithParsedBlocks ); const availableParsedPatterns = parsedPatterns.filter( ( pattern ) => @@ -2467,7 +2475,7 @@ export const __experimentalGetPatternsByBlockTypes = createRegistrySelector( * Determines the items that appear in the available pattern transforms list. * * For now we only handle blocks without InnerBlocks and take into account - * the `__experimentalRole` property of blocks' attributes for the transformation. + * the `role` property of blocks' attributes for the transformation. * * We return the first set of possible eligible block patterns, * by checking the `blockTypes` property. We still have to recurse through @@ -2489,7 +2497,7 @@ export const __experimentalGetPatternTransformItems = createRegistrySelector( } /** * For now we only handle blocks without InnerBlocks and take into account - * the `__experimentalRole` property of blocks' attributes for the transformation. + * the `role` property of blocks' attributes for the transformation. * Note that the blocks have been retrieved through `getBlock`, which doesn't * return the inner blocks of an inner block controller, so we still need * to check for this case too. @@ -2926,6 +2934,7 @@ export const getBlockEditingMode = createRegistrySelector( if ( clientId === null ) { clientId = ''; } + // In zoom-out mode, override the behavior set by // __unstableSetBlockEditingMode to only allow editing the top-level // sections. @@ -2943,9 +2952,13 @@ export const getBlockEditingMode = createRegistrySelector( state, sectionRootClientId ); - if ( ! sectionsClientIds?.includes( clientId ) ) { - return 'disabled'; + + // Sections are always contentOnly. + if ( sectionsClientIds?.includes( clientId ) ) { + return 'contentOnly'; } + + return 'disabled'; } const blockEditingMode = state.blockEditingModes.get( clientId ); @@ -2959,10 +2972,10 @@ export const getBlockEditingMode = createRegistrySelector( const templateLock = getTemplateLock( state, rootClientId ); if ( templateLock === 'contentOnly' ) { const name = getBlockName( state, clientId ); - const isContent = - select( blocksStore ).__experimentalHasContentRoleAttribute( - name - ); + const { hasContentRoleAttribute } = unlock( + select( blocksStore ) + ); + const isContent = hasContentRoleAttribute( name ); return isContent ? 'contentOnly' : 'disabled'; } const parentMode = getBlockEditingMode( state, rootClientId ); diff --git a/packages/block-editor/src/store/test/private-selectors.js b/packages/block-editor/src/store/test/private-selectors.js index 45432b750bb9eb..657069721034fe 100644 --- a/packages/block-editor/src/store/test/private-selectors.js +++ b/packages/block-editor/src/store/test/private-selectors.js @@ -124,10 +124,10 @@ describe( 'private selectors', () => { blockEditingModes: new Map( [] ), }; - const __experimentalHasContentRoleAttribute = jest.fn( () => false ); + const hasContentRoleAttribute = jest.fn( () => false ); getBlockEditingMode.registry = { select: jest.fn( () => ( { - __experimentalHasContentRoleAttribute, + hasContentRoleAttribute, } ) ), }; diff --git a/packages/block-editor/src/store/test/selectors.js b/packages/block-editor/src/store/test/selectors.js index 85006621c4701e..bac38a9c455657 100644 --- a/packages/block-editor/src/store/test/selectors.js +++ b/packages/block-editor/src/store/test/selectors.js @@ -15,6 +15,7 @@ import { select, dispatch } from '@wordpress/data'; */ import * as selectors from '../selectors'; import { store } from '../'; +import { lock } from '../../lock-unlock'; const { getBlockName, @@ -4372,11 +4373,14 @@ describe( 'getBlockEditingMode', () => { blockEditingModes: new Map( [] ), }; - const __experimentalHasContentRoleAttribute = jest.fn( () => false ); + const hasContentRoleAttribute = jest.fn( () => false ); + + const fauxPrivateAPIs = {}; + + lock( fauxPrivateAPIs, { hasContentRoleAttribute } ); + getBlockEditingMode.registry = { - select: jest.fn( () => ( { - __experimentalHasContentRoleAttribute, - } ) ), + select: jest.fn( () => fauxPrivateAPIs ), }; it( 'should return default by default', () => { @@ -4480,7 +4484,7 @@ describe( 'getBlockEditingMode', () => { }, }, }; - __experimentalHasContentRoleAttribute.mockReturnValueOnce( false ); + hasContentRoleAttribute.mockReturnValueOnce( false ); expect( getBlockEditingMode( state, 'b3247f75-fd94-4fef-97f9-5bfd162cc416' ) ).toBe( 'disabled' ); @@ -4496,7 +4500,7 @@ describe( 'getBlockEditingMode', () => { }, }, }; - __experimentalHasContentRoleAttribute.mockReturnValueOnce( true ); + hasContentRoleAttribute.mockReturnValueOnce( true ); expect( getBlockEditingMode( state, 'b3247f75-fd94-4fef-97f9-5bfd162cc416' ) ).toBe( 'contentOnly' ); diff --git a/packages/block-editor/src/style.scss b/packages/block-editor/src/style.scss index e6ec77b55a0ec5..512169351fe1fb 100644 --- a/packages/block-editor/src/style.scss +++ b/packages/block-editor/src/style.scss @@ -30,6 +30,7 @@ @import "./components/global-styles/style.scss"; @import "./components/grid/style.scss"; @import "./components/height-control/style.scss"; +@import "./components/iframe/style.scss"; @import "./components/image-size-control/style.scss"; @import "./components/inserter-list-item/style.scss"; @import "./components/inspector-controls-tabs/style.scss"; diff --git a/packages/block-editor/src/utils/block-bindings.js b/packages/block-editor/src/utils/block-bindings.js index b3daf4f4b36b43..ed59115a7fdbb3 100644 --- a/packages/block-editor/src/utils/block-bindings.js +++ b/packages/block-editor/src/utils/block-bindings.js @@ -13,8 +13,58 @@ function isObjectEmpty( object ) { return ! object || Object.keys( object ).length === 0; } -export function useBlockBindingsUtils() { - const { clientId } = useBlockEditContext(); +/** + * Contains utils to update the block `bindings` metadata. + * + * @typedef {Object} WPBlockBindingsUtils + * + * @property {Function} updateBlockBindings Updates the value of the bindings connected to block attributes. + * @property {Function} removeAllBlockBindings Removes the bindings property of the `metadata` attribute. + */ + +/** + * Retrieves the existing utils needed to update the block `bindings` metadata. + * They can be used to create, modify, or remove connections from the existing block attributes. + * + * It contains the following utils: + * - `updateBlockBindings`: Updates the value of the bindings connected to block attributes. It can be used to remove a specific binding by setting the value to `undefined`. + * - `removeAllBlockBindings`: Removes the bindings property of the `metadata` attribute. + * + * @param {?string} clientId Optional block client ID. If not set, it will use the current block client ID from the context. + * + * @return {?WPBlockBindingsUtils} Object containing the block bindings utils. + * + * @example + * ```js + * import { useBlockBindingsUtils } from '@wordpress/block-editor' + * const { updateBlockBindings, removeAllBlockBindings } = useBlockBindingsUtils(); + * + * // Update url and alt attributes. + * updateBlockBindings( { + * url: { + * source: 'core/post-meta', + * args: { + * key: 'url_custom_field', + * }, + * }, + * alt: { + * source: 'core/post-meta', + * args: { + * key: 'text_custom_field', + * }, + * }, + * } ); + * + * // Remove binding from url attribute. + * updateBlockBindings( { url: undefined } ); + * + * // Remove bindings from all attributes. + * removeAllBlockBindings(); + * ``` + */ +export function useBlockBindingsUtils( clientId ) { + const { clientId: contextClientId } = useBlockEditContext(); + const blockClientId = clientId || contextClientId; const { updateBlockAttributes } = useDispatch( blockEditorStore ); const { getBlockAttributes } = useRegistry().select( blockEditorStore ); @@ -49,7 +99,7 @@ export function useBlockBindingsUtils() { */ const updateBlockBindings = ( bindings ) => { const { metadata: { bindings: currentBindings, ...metadata } = {} } = - getBlockAttributes( clientId ); + getBlockAttributes( blockClientId ); const newBindings = { ...currentBindings }; Object.entries( bindings ).forEach( ( [ attribute, binding ] ) => { @@ -69,7 +119,7 @@ export function useBlockBindingsUtils() { delete newMetadata.bindings; } - updateBlockAttributes( clientId, { + updateBlockAttributes( blockClientId, { metadata: isObjectEmpty( newMetadata ) ? undefined : newMetadata, } ); }; @@ -87,8 +137,8 @@ export function useBlockBindingsUtils() { */ const removeAllBlockBindings = () => { const { metadata: { bindings, ...metadata } = {} } = - getBlockAttributes( clientId ); - updateBlockAttributes( clientId, { + getBlockAttributes( blockClientId ); + updateBlockAttributes( blockClientId, { metadata: isObjectEmpty( metadata ) ? undefined : metadata, } ); }; diff --git a/packages/block-editor/src/utils/dom.js b/packages/block-editor/src/utils/dom.js index 9c2e813ef742b6..6d55f2468d24b6 100644 --- a/packages/block-editor/src/utils/dom.js +++ b/packages/block-editor/src/utils/dom.js @@ -119,38 +119,63 @@ function isElementVisible( element ) { } /** - * Returns the rect of the element including all visible nested elements. + * Checks if the element is scrollable. * - * Visible nested elements, including elements that overflow the parent, are - * taken into account. - * - * This function is useful for calculating the visible area of a block that - * contains nested elements that overflow the block, e.g. the Navigation block, - * which can contain overflowing Submenu blocks. + * @param {Element} element Element. + * @return {boolean} True if the element is scrollable. + */ +function isScrollable( element ) { + const style = window.getComputedStyle( element ); + return ( + style.overflowX === 'auto' || + style.overflowX === 'scroll' || + style.overflowY === 'auto' || + style.overflowY === 'scroll' + ); +} + +export const WITH_OVERFLOW_ELEMENT_BLOCKS = [ 'core/navigation' ]; +/** + * Returns the bounding rectangle of an element, with special handling for blocks + * that have visible overflowing children (defined in WITH_OVERFLOW_ELEMENT_BLOCKS). * + * For blocks like Navigation that can have overflowing elements (e.g. submenus), + * this function calculates the combined bounds of both the parent and its visible + * children. The returned rect may extend beyond the viewport. * The returned rect represents the full extent of the element and its visible * children, which may extend beyond the viewport. * * @param {Element} element Element. * @return {DOMRect} Bounding client rect of the element and its visible children. */ -export function getVisibleElementBounds( element ) { +export function getElementBounds( element ) { const viewport = element.ownerDocument.defaultView; + if ( ! viewport ) { return new window.DOMRectReadOnly(); } let bounds = element.getBoundingClientRect(); + const dataType = element.getAttribute( 'data-type' ); - const stack = [ element ]; - let currentElement; - - while ( ( currentElement = stack.pop() ) ) { - for ( const child of currentElement.children ) { - if ( isElementVisible( child ) ) { - const childBounds = child.getBoundingClientRect(); - bounds = rectUnion( bounds, childBounds ); - stack.push( child ); + /* + * For blocks with overflowing elements (like Navigation), include the bounds + * of visible children that extend beyond the parent container. + */ + if ( dataType && WITH_OVERFLOW_ELEMENT_BLOCKS.includes( dataType ) ) { + const stack = [ element ]; + let currentElement; + + while ( ( currentElement = stack.pop() ) ) { + // Children won’t affect bounds unless the element is not scrollable. + if ( ! isScrollable( currentElement ) ) { + for ( const child of currentElement.children ) { + if ( isElementVisible( child ) ) { + const childBounds = child.getBoundingClientRect(); + bounds = rectUnion( bounds, childBounds ); + stack.push( child ); + } + } } } } diff --git a/packages/block-editor/src/utils/get-font-styles-and-weights.js b/packages/block-editor/src/utils/get-font-styles-and-weights.js index dcb56fc86b2673..c832582fa524d5 100644 --- a/packages/block-editor/src/utils/get-font-styles-and-weights.js +++ b/packages/block-editor/src/utils/get-font-styles-and-weights.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { _x, __, sprintf } from '@wordpress/i18n'; +import { _x, sprintf } from '@wordpress/i18n'; /** * Internal dependencies @@ -173,7 +173,7 @@ export function getFontStylesAndWeights( fontFamilyFaces ) { ? weightName : sprintf( /* translators: 1: Font weight name. 2: Font style name. */ - __( '%1$s %2$s' ), + _x( '%1$s %2$s', 'font' ), weightName, styleName ); diff --git a/packages/block-editor/src/utils/index.js b/packages/block-editor/src/utils/index.js index 6f53ba585e5ecb..1b5aa769a13b28 100644 --- a/packages/block-editor/src/utils/index.js +++ b/packages/block-editor/src/utils/index.js @@ -1,2 +1,3 @@ export { default as transformStyles } from './transform-styles'; export { default as getPxFromCssUnit } from './get-px-from-css-unit'; +export { useBlockBindingsUtils } from './block-bindings'; diff --git a/packages/block-editor/src/utils/test/dom.js b/packages/block-editor/src/utils/test/dom.js new file mode 100644 index 00000000000000..50a16a27cf9617 --- /dev/null +++ b/packages/block-editor/src/utils/test/dom.js @@ -0,0 +1,224 @@ +/** + * Internal dependencies + */ +import { getElementBounds, WITH_OVERFLOW_ELEMENT_BLOCKS } from '../dom'; +describe( 'dom', () => { + describe( 'getElementBounds', () => { + it( 'should return a DOMRectReadOnly object if the viewport is not available', () => { + const element = { + ownerDocument: { + defaultView: null, + }, + }; + expect( getElementBounds( element ) ).toEqual( + new window.DOMRectReadOnly() + ); + } ); + it( 'should return a DOMRectReadOnly object if the viewport is available', () => { + const element = { + ownerDocument: { + defaultView: { + getComputedStyle: () => ( { + display: 'block', + visibility: 'visible', + opacity: '1', + } ), + }, + }, + getBoundingClientRect: () => ( { + left: 0, + top: 0, + right: 100, + bottom: 100, + width: 100, + height: 100, + } ), + getAttribute: ( x ) => x, + }; + expect( getElementBounds( element ) ).toEqual( + new window.DOMRectReadOnly( 0, 0, 100, 100 ) + ); + } ); + it( 'should clip left and right values when an element is larger than the viewport width', () => { + const element = window.document.createElement( 'div' ); + element.getBoundingClientRect = jest.fn().mockReturnValue( { + left: -10, + top: 0, + right: window.innerWidth + 10, + bottom: 100, + width: window.innerWidth, + height: 100, + } ); + expect( getElementBounds( element ).toJSON() ).toEqual( { + left: 0, // Reset to min left bound. + top: 0, + right: window.innerWidth, // Reset to max right bound. + bottom: 100, + width: window.innerWidth, + height: 100, + x: 0, + y: 0, + } ); + } ); + it( 'should return the parent DOMRectReadOnly object if the parent block type is not supported', () => { + const element = window.document.createElement( 'div' ); + element.getBoundingClientRect = jest.fn().mockReturnValue( { + left: 0, + top: 0, + right: 100, + bottom: 100, + width: 100, + height: 100, + } ); + element.setAttribute( 'data-type', 'test' ); + const childElement = window.document.createElement( 'div' ); + childElement.getBoundingClientRect = jest.fn().mockReturnValue( { + left: 0, + top: 0, + right: 333, + bottom: 333, + width: 333, + height: 333, + x: 0, + y: 0, + } ); + element.appendChild( childElement ); + + expect( getElementBounds( element ).toJSON() ).toEqual( { + left: 0, + top: 0, + right: 100, + bottom: 100, + width: 100, + height: 100, + x: 0, + y: 0, + } ); + } ); + describe( 'With known block type', () => { + it( 'should return the child DOMRectReadOnly object if it is visible and a known block type', () => { + const element = window.document.createElement( 'div' ); + element.getBoundingClientRect = jest.fn().mockReturnValue( { + left: 0, + top: 0, + right: 100, + bottom: 100, + width: 100, + height: 100, + } ); + element.setAttribute( + 'data-type', + WITH_OVERFLOW_ELEMENT_BLOCKS[ 0 ] + ); + const childElement = window.document.createElement( 'div' ); + childElement.getBoundingClientRect = jest + .fn() + .mockReturnValue( { + left: 0, + top: 0, + right: 333, + bottom: 333, + width: 333, + height: 333, + x: 0, + y: 0, + } ); + element.appendChild( childElement ); + + expect( getElementBounds( element ).toJSON() ).toEqual( { + left: 0, + top: 0, + right: 333, + bottom: 333, + width: 333, + height: 333, + x: 0, + y: 0, + } ); + } ); + it( 'should return the parent DOMRectReadOnly if the child is scrollable', () => { + const element = window.document.createElement( 'div' ); + element.setAttribute( + 'data-type', + WITH_OVERFLOW_ELEMENT_BLOCKS[ 0 ] + ); + element.style.overflowX = 'auto'; + element.style.overflowY = 'auto'; + element.getBoundingClientRect = jest.fn().mockReturnValue( { + left: 0, + top: 0, + right: 100, + bottom: 100, + width: 100, + height: 100, + } ); + const childElement = window.document.createElement( 'div' ); + childElement.getBoundingClientRect = jest + .fn() + .mockReturnValue( { + left: 0, + top: 0, + right: 333, + bottom: 333, + width: 333, + height: 333, + x: 0, + y: 0, + } ); + element.appendChild( childElement ); + + expect( getElementBounds( element ).toJSON() ).toEqual( { + left: 0, + top: 0, + right: 100, + bottom: 100, + width: 100, + height: 100, + x: 0, + y: 0, + } ); + } ); + it( 'should return the parent DOMRectReadOnly object if the child element is not visible', () => { + const element = window.document.createElement( 'div' ); + element.getBoundingClientRect = jest.fn().mockReturnValue( { + left: 0, + top: 0, + right: 100, + bottom: 100, + width: 100, + height: 100, + } ); + element.setAttribute( + 'data-type', + WITH_OVERFLOW_ELEMENT_BLOCKS[ 0 ] + ); + const childElement = window.document.createElement( 'div' ); + childElement.getBoundingClientRect = jest + .fn() + .mockReturnValue( { + left: 0, + top: 0, + right: 333, + bottom: 333, + width: 333, + height: 333, + x: 0, + y: 0, + } ); + childElement.style.display = 'none'; + element.appendChild( childElement ); + + expect( getElementBounds( element ).toJSON() ).toEqual( { + left: 0, + top: 0, + right: 100, + bottom: 100, + width: 100, + height: 100, + x: 0, + y: 0, + } ); + } ); + } ); + } ); +} ); diff --git a/packages/block-editor/src/utils/test/use-block-bindings-utils.js b/packages/block-editor/src/utils/test/use-block-bindings-utils.js new file mode 100644 index 00000000000000..e5e26bd24c20d1 --- /dev/null +++ b/packages/block-editor/src/utils/test/use-block-bindings-utils.js @@ -0,0 +1,174 @@ +/** + * External dependencies + */ +import { renderHook } from '@testing-library/react'; + +/** + * WordPress dependencies + */ +import { store as blockEditorStore } from '@wordpress/block-editor'; +import { dispatch, select } from '@wordpress/data'; +import { + createBlock, + getBlockTypes, + unregisterBlockType, +} from '@wordpress/blocks'; +import { registerCoreBlocks } from '@wordpress/block-library'; + +/** + * Internal dependencies + */ +import { useBlockBindingsUtils } from '../'; + +describe( 'useBlockBindingsUtils', () => { + beforeAll( () => { + // Register all core blocks + registerCoreBlocks(); + } ); + + let clientId; + beforeEach( async () => { + const block = createBlock( 'core/paragraph', { + metadata: { + name: 'Block name', + bindings: { + prop1: { + source: 'core/post-meta', + args: { + key: 'initial_key', + }, + }, + prop2: { + source: 'core/post-meta', + args: { + key: 'initial_key', + }, + }, + }, + }, + } ); + await dispatch( blockEditorStore ).resetBlocks( [ block ] ); + clientId = block.clientId; + } ); + + afterAll( () => { + // Remove blocks after all tests. + dispatch( blockEditorStore ).resetBlocks( [] ); + + // Clean up registered blocks + getBlockTypes().forEach( ( block ) => { + unregisterBlockType( block.name ); + } ); + } ); + + it( 'should be possible to update just one connection', async () => { + renderHook( () => { + const { updateBlockBindings } = useBlockBindingsUtils( clientId ); + updateBlockBindings( { + prop1: { + source: 'core/post-meta', + args: { + key: 'new_key', + }, + }, + } ); + } ); + const { metadata } = + await select( blockEditorStore ).getBlockAttributes( clientId ); + expect( metadata ).toMatchObject( { + // Other metadata properties shouldn't change. + name: 'Block name', + bindings: { + prop1: { + source: 'core/post-meta', + args: { + key: 'new_key', + }, + }, + prop2: { + source: 'core/post-meta', + args: { + key: 'initial_key', + }, + }, + }, + } ); + } ); + + it( 'should be possible to update multiple connections at once', async () => { + renderHook( () => { + const { updateBlockBindings } = useBlockBindingsUtils( clientId ); + updateBlockBindings( { + prop1: { + source: 'core/post-meta', + args: { + key: 'new_key', + }, + }, + prop2: { + source: 'core/post-meta', + args: { + key: 'new_key', + }, + }, + } ); + } ); + const { metadata } = + await select( blockEditorStore ).getBlockAttributes( clientId ); + expect( metadata ).toMatchObject( { + // Other metadata properties shouldn't change. + name: 'Block name', + bindings: { + prop1: { + source: 'core/post-meta', + args: { + key: 'new_key', + }, + }, + prop2: { + source: 'core/post-meta', + args: { + key: 'new_key', + }, + }, + }, + } ); + } ); + + it( 'should be possible to remove connections', async () => { + renderHook( () => { + const { updateBlockBindings } = useBlockBindingsUtils( clientId ); + updateBlockBindings( { + prop2: undefined, + } ); + } ); + const { metadata } = + await select( blockEditorStore ).getBlockAttributes( clientId ); + expect( metadata ).toMatchObject( { + // Other metadata properties shouldn't change. + name: 'Block name', + bindings: { + prop1: { + source: 'core/post-meta', + args: { + key: 'initial_key', + }, + }, + }, + } ); + } ); + + it( 'should be possible to remove all connections', async () => { + renderHook( () => { + const { removeAllBlockBindings } = + useBlockBindingsUtils( clientId ); + removeAllBlockBindings(); + } ); + const { metadata } = + await select( blockEditorStore ).getBlockAttributes( clientId ); + expect( metadata ).toMatchObject( { + // Other metadata properties shouldn't change. + name: 'Block name', + } ); + } ); +} ); diff --git a/packages/block-library/CHANGELOG.md b/packages/block-library/CHANGELOG.md index 4ee3ebc7564b53..08accf8c36d9e5 100644 --- a/packages/block-library/CHANGELOG.md +++ b/packages/block-library/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 9.8.0 (2024-09-19) + ## 9.7.0 (2024-09-05) ## 9.6.0 (2024-08-21) diff --git a/packages/block-library/package.json b/packages/block-library/package.json index 1353ef24c77d89..1a4c50171d81d1 100644 --- a/packages/block-library/package.json +++ b/packages/block-library/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/block-library", - "version": "9.7.0", + "version": "9.8.15", "description": "Block library for the WordPress editor.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/block-library/src/audio/block.json b/packages/block-library/src/audio/block.json index bee2ff6d534a70..9b77efee23cce2 100644 --- a/packages/block-library/src/audio/block.json +++ b/packages/block-library/src/audio/block.json @@ -10,24 +10,24 @@ "attributes": { "blob": { "type": "string", - "__experimentalRole": "local" + "role": "local" }, "src": { "type": "string", "source": "attribute", "selector": "audio", "attribute": "src", - "__experimentalRole": "content" + "role": "content" }, "caption": { "type": "rich-text", "source": "rich-text", "selector": "figcaption", - "__experimentalRole": "content" + "role": "content" }, "id": { "type": "number", - "__experimentalRole": "content" + "role": "content" }, "autoplay": { "type": "boolean", diff --git a/packages/block-library/src/avatar/hooks.js b/packages/block-library/src/avatar/hooks.js index 1f84b5c107682b..d73b604bdef22e 100644 --- a/packages/block-library/src/avatar/hooks.js +++ b/packages/block-library/src/avatar/hooks.js @@ -47,9 +47,8 @@ export function useCommentAvatar( { commentId } ) { src: avatarUrls ? avatarUrls[ avatarUrls.length - 1 ] : defaultAvatar, minSize, maxSize, - // translators: %s is the Author name. alt: authorName - ? // translators: %s is the Author name. + ? // translators: %s: Author name. sprintf( __( '%s Avatar' ), authorName ) : __( 'Default Avatar' ), }; @@ -89,7 +88,7 @@ export function useUserAvatar( { userId, postId, postType } ) { minSize, maxSize, alt: authorDetails - ? // translators: %s is the Author name. + ? // translators: %s: Author name. sprintf( __( '%s Avatar' ), authorDetails?.name ) : __( 'Default Avatar' ), }; diff --git a/packages/block-library/src/avatar/index.php b/packages/block-library/src/avatar/index.php index cc86052af4abe2..d4a01c1c3a1e77 100644 --- a/packages/block-library/src/avatar/index.php +++ b/packages/block-library/src/avatar/index.php @@ -46,7 +46,7 @@ function render_block_core_avatar( $attributes, $content, $block ) { } $author_name = get_the_author_meta( 'display_name', $author_id ); - // translators: %s is the Author name. + // translators: %s: Author name. $alt = sprintf( __( '%s Avatar' ), $author_name ); $avatar_block = get_avatar( $author_id, @@ -64,7 +64,7 @@ function render_block_core_avatar( $attributes, $content, $block ) { // translators: %s is the Author name. $label = 'aria-label="' . esc_attr( sprintf( __( '(%s author archive, opens in a new tab)' ), $author_name ) ) . '"'; } - // translators: %1$s: Author archive link. %2$s: Link target. %3$s Aria label. %4$s Avatar image. + // translators: 1: Author archive link. 2: Link target. %3$s Aria label. %4$s Avatar image. $avatar_block = sprintf( '%4$s', esc_url( get_author_posts_url( $author_id ) ), esc_attr( $attributes['linkTarget'] ), $label, $avatar_block ); } return sprintf( '
%2s
', $wrapper_attributes, $avatar_block ); @@ -73,7 +73,7 @@ function render_block_core_avatar( $attributes, $content, $block ) { if ( ! $comment ) { return ''; } - /* translators: %s is the Comment Author name */ + /* translators: %s: Author name. */ $alt = sprintf( __( '%s Avatar' ), $comment->comment_author ); $avatar_block = get_avatar( $comment, @@ -88,10 +88,9 @@ function render_block_core_avatar( $attributes, $content, $block ) { if ( isset( $attributes['isLink'] ) && $attributes['isLink'] && isset( $comment->comment_author_url ) && '' !== $comment->comment_author_url ) { $label = ''; if ( '_blank' === $attributes['linkTarget'] ) { - // translators: %s is the Comment Author name. + // translators: %s: Comment author name. $label = 'aria-label="' . esc_attr( sprintf( __( '(%s website link, opens in a new tab)' ), $comment->comment_author ) ) . '"'; } - // translators: %1$s: Comment Author website link. %2$s: Link target. %3$s Aria label. %4$s Avatar image. $avatar_block = sprintf( '%4$s', esc_url( $comment->comment_author_url ), esc_attr( $attributes['linkTarget'] ), $label, $avatar_block ); } return sprintf( '
%2s
', $wrapper_attributes, $avatar_block ); diff --git a/packages/block-library/src/block/edit.js b/packages/block-library/src/block/edit.js index 5c90361e6bb435..104b07157cba74 100644 --- a/packages/block-library/src/block/edit.js +++ b/packages/block-library/src/block/edit.js @@ -32,7 +32,7 @@ import { InnerBlocks, } from '@wordpress/block-editor'; import { privateApis as patternsPrivateApis } from '@wordpress/patterns'; -import { store as blocksStore } from '@wordpress/blocks'; +import { getBlockBindingsSource } from '@wordpress/blocks'; /** * Internal dependencies @@ -196,7 +196,6 @@ function ReusableBlockEdit( { ( select ) => { const { getBlocks, getSettings, getBlockEditingMode } = select( blockEditorStore ); - const { getBlockBindingsSource } = unlock( select( blocksStore ) ); // For editing link to the site editor if the theme and user permissions support it. return { innerBlocks: getBlocks( patternClientId ), diff --git a/packages/block-library/src/button/block.json b/packages/block-library/src/button/block.json index d0f90b93467c9d..2c1c05baa20dd3 100644 --- a/packages/block-library/src/button/block.json +++ b/packages/block-library/src/button/block.json @@ -26,34 +26,34 @@ "source": "attribute", "selector": "a", "attribute": "href", - "__experimentalRole": "content" + "role": "content" }, "title": { "type": "string", "source": "attribute", "selector": "a,button", "attribute": "title", - "__experimentalRole": "content" + "role": "content" }, "text": { "type": "rich-text", "source": "rich-text", "selector": "a,button", - "__experimentalRole": "content" + "role": "content" }, "linkTarget": { "type": "string", "source": "attribute", "selector": "a", "attribute": "target", - "__experimentalRole": "content" + "role": "content" }, "rel": { "type": "string", "source": "attribute", "selector": "a", "attribute": "rel", - "__experimentalRole": "content" + "role": "content" }, "placeholder": { "type": "string" diff --git a/packages/block-library/src/button/edit.js b/packages/block-library/src/button/edit.js index d7b8e6486c3c66..3539fd54f4eece 100644 --- a/packages/block-library/src/button/edit.js +++ b/packages/block-library/src/button/edit.js @@ -9,7 +9,6 @@ import clsx from 'clsx'; import { NEW_TAB_TARGET, NOFOLLOW_REL } from './constants'; import { getUpdatedLinkAttributes } from './get-updated-link-attributes'; import removeAnchorTag from '../utils/remove-anchor-tag'; -import { unlock } from '../lock-unlock'; /** * WordPress dependencies @@ -45,7 +44,7 @@ import { createBlock, cloneBlock, getDefaultBlockName, - store as blocksStore, + getBlockBindingsSource, } from '@wordpress/blocks'; import { useMergeRefs, useRefEffect } from '@wordpress/compose'; import { useSelect, useDispatch } from '@wordpress/data'; @@ -240,9 +239,9 @@ function ButtonEdit( props ) { return {}; } - const blockBindingsSource = unlock( - select( blocksStore ) - ).getBlockBindingsSource( metadata?.bindings?.url?.source ); + const blockBindingsSource = getBlockBindingsSource( + metadata?.bindings?.url?.source + ); return { lockUrlControls: diff --git a/packages/block-library/src/categories/block.json b/packages/block-library/src/categories/block.json index bfd8461f8eda43..3609bdf9ab97c0 100644 --- a/packages/block-library/src/categories/block.json +++ b/packages/block-library/src/categories/block.json @@ -34,7 +34,7 @@ }, "label": { "type": "string", - "__experimentalRole": "content" + "role": "content" }, "showLabel": { "type": "boolean", diff --git a/packages/block-library/src/categories/index.php b/packages/block-library/src/categories/index.php index e15f662bdfbb9b..60a29713b4660d 100644 --- a/packages/block-library/src/categories/index.php +++ b/packages/block-library/src/categories/index.php @@ -49,7 +49,7 @@ function render_block_core_categories( $attributes, $content, $block ) { $show_label = empty( $attributes['showLabel'] ) ? ' screen-reader-text' : ''; $default_label = $taxonomy->label; - $label_text = ! empty( $attributes['label'] ) ? $attributes['label'] : $default_label; + $label_text = ! empty( $attributes['label'] ) ? wp_kses_post( $attributes['label'] ) : $default_label; $wrapper_markup = '
%2$s
'; $items_markup = wp_dropdown_categories( $args ); $type = 'dropdown'; diff --git a/packages/block-library/src/comment-author-avatar/index.php b/packages/block-library/src/comment-author-avatar/index.php index 7c568fc3ded5e4..1568b6606669c4 100644 --- a/packages/block-library/src/comment-author-avatar/index.php +++ b/packages/block-library/src/comment-author-avatar/index.php @@ -46,7 +46,7 @@ function render_block_core_comment_author_avatar( $attributes, $content, $block $styles = isset( $wrapper_attributes['style'] ) ? $wrapper_attributes['style'] : ''; $classes = isset( $wrapper_attributes['class'] ) ? $wrapper_attributes['class'] : ''; - /* translators: %s is the Comment Author name */ + /* translators: %s: Author name. */ $alt = sprintf( __( '%s Avatar' ), $comment->comment_author ); $avatar_block = get_avatar( diff --git a/packages/block-library/src/cover/edit/index.js b/packages/block-library/src/cover/edit/index.js index ec62bd58a2c33a..804027708881b6 100644 --- a/packages/block-library/src/cover/edit/index.js +++ b/packages/block-library/src/cover/edit/index.js @@ -18,6 +18,7 @@ import { useInnerBlocksProps, __experimentalUseGradient, store as blockEditorStore, + useBlockEditingMode, } from '@wordpress/block-editor'; import { __ } from '@wordpress/i18n'; import { useSelect, useDispatch } from '@wordpress/data'; @@ -278,6 +279,9 @@ function CoverEdit( { const isImageBackground = IMAGE_BACKGROUND_TYPE === backgroundType; const isVideoBackground = VIDEO_BACKGROUND_TYPE === backgroundType; + const blockEditingMode = useBlockEditingMode(); + const hasNonContentControls = blockEditingMode === 'default'; + const [ resizeListener, { height, width } ] = useResizeObserver(); const resizableBoxDimensions = useMemo( () => { return { @@ -447,7 +451,7 @@ function CoverEdit( { <> { blockControls } { inspectorControls } - { isSelected && ( + { hasNonContentControls && isSelected && ( ) }
- { isSelected && ( + { hasNonContentControls && isSelected && ( ) } diff --git a/packages/block-library/src/cover/index.php b/packages/block-library/src/cover/index.php index 2fca0b0374dd29..1ffe7ab3f4dbc6 100644 --- a/packages/block-library/src/cover/index.php +++ b/packages/block-library/src/cover/index.php @@ -20,29 +20,22 @@ function render_block_core_cover( $attributes, $content ) { return $content; } + $object_position = isset( $attributes['focalPoint'] ) + ? round( $attributes['focalPoint']['x'] * 100 ) . '% ' . round( $attributes['focalPoint']['y'] * 100 ) . '%' + : null; + if ( ! ( $attributes['hasParallax'] || $attributes['isRepeated'] ) ) { $attr = array( 'class' => 'wp-block-cover__image-background', 'data-object-fit' => 'cover', ); - if ( isset( $attributes['focalPoint'] ) ) { - $object_position = round( $attributes['focalPoint']['x'] * 100 ) . '% ' . round( $attributes['focalPoint']['y'] * 100 ) . '%'; + if ( $object_position ) { $attr['data-object-position'] = $object_position; - $attr['style'] = 'object-position: ' . $object_position; + $attr['style'] = 'object-position:' . $object_position . ';'; } $image = get_the_post_thumbnail( null, 'post-thumbnail', $attr ); - - /* - * Inserts the featured image between the (1st) cover 'background' `span` and 'inner_container' `div`, - * and removes eventual whitespace characters between the two (typically introduced at template level) - */ - $inner_container_start = '/]+wp-block-cover__inner-container[\s|"][^>]*>/U'; - if ( 1 === preg_match( $inner_container_start, $content, $matches, PREG_OFFSET_CAPTURE ) ) { - $offset = $matches[0][1]; - $content = substr( $content, 0, $offset ) . $image . substr( $content, $offset ); - } } else { if ( in_the_loop() ) { update_post_thumbnail_cache(); @@ -52,15 +45,41 @@ function render_block_core_cover( $attributes, $content ) { return $content; } - $processor = new WP_HTML_Tag_Processor( $content ); + $current_thumbnail_id = get_post_thumbnail_id(); + + $processor = new WP_HTML_Tag_Processor( '
' ); $processor->next_tag(); - $styles = $processor->get_attribute( 'style' ); - $merged_styles = ! empty( $styles ) ? $styles . ';' : ''; - $merged_styles .= 'background-image:url(' . esc_url( $current_featured_image ) . ');'; + $current_alt = trim( strip_tags( get_post_meta( $current_thumbnail_id, '_wp_attachment_image_alt', true ) ) ); + if ( $current_alt ) { + $processor->set_attribute( 'role', 'img' ); + $processor->set_attribute( 'aria-label', $current_alt ); + } + + $processor->add_class( 'wp-block-cover__image-background' ); + $processor->add_class( 'wp-image-' . $current_thumbnail_id ); + if ( $attributes['hasParallax'] ) { + $processor->add_class( 'has-parallax' ); + } + if ( $attributes['isRepeated'] ) { + $processor->add_class( 'is-repeated' ); + } + + $styles = 'background-position:' . ( $object_position ?? '50% 50%' ) . ';'; + $styles .= 'background-image:url(' . esc_url( $current_featured_image ) . ');'; + $processor->set_attribute( 'style', $styles ); + + $image = $processor->get_updated_html(); + } - $processor->set_attribute( 'style', $merged_styles ); - $content = $processor->get_updated_html(); + /* + * Inserts the featured image between the (1st) cover 'background' `span` and 'inner_container' `div`, + * and removes eventual whitespace characters between the two (typically introduced at template level) + */ + $inner_container_start = '/]+wp-block-cover__inner-container[\s|"][^>]*>/U'; + if ( 1 === preg_match( $inner_container_start, $content, $matches, PREG_OFFSET_CAPTURE ) ) { + $offset = $matches[0][1]; + $content = substr( $content, 0, $offset ) . $image . substr( $content, $offset ); } return $content; diff --git a/packages/block-library/src/cover/style.scss b/packages/block-library/src/cover/style.scss index 41b3acf1833fc0..b2c1fe88440235 100644 --- a/packages/block-library/src/cover/style.scss +++ b/packages/block-library/src/cover/style.scss @@ -100,6 +100,12 @@ color: inherit; // Reset the fixed LTR direction at the root of the block in RTL languages. /*rtl:raw: direction: rtl; */ + + // Reset the z-index to auto when the body has a modal open. So when the + // modal is inside the cover, it doesn't create a stacking context. + @at-root .has-modal-open & { + z-index: auto; + } } // Position: Top diff --git a/packages/block-library/src/embed/block.json b/packages/block-library/src/embed/block.json index a42aafbab4b0b9..5bfb63b0fa9e94 100644 --- a/packages/block-library/src/embed/block.json +++ b/packages/block-library/src/embed/block.json @@ -9,21 +9,21 @@ "attributes": { "url": { "type": "string", - "__experimentalRole": "content" + "role": "content" }, "caption": { "type": "rich-text", "source": "rich-text", "selector": "figcaption", - "__experimentalRole": "content" + "role": "content" }, "type": { "type": "string", - "__experimentalRole": "content" + "role": "content" }, "providerNameSlug": { "type": "string", - "__experimentalRole": "content" + "role": "content" }, "allowResponsive": { "type": "boolean", @@ -32,12 +32,12 @@ "responsive": { "type": "boolean", "default": false, - "__experimentalRole": "content" + "role": "content" }, "previewable": { "type": "boolean", "default": true, - "__experimentalRole": "content" + "role": "content" } }, "supports": { diff --git a/packages/block-library/src/file/block.json b/packages/block-library/src/file/block.json index 0526120c4dfc1e..bf0082c576dd16 100644 --- a/packages/block-library/src/file/block.json +++ b/packages/block-library/src/file/block.json @@ -13,7 +13,7 @@ }, "blob": { "type": "string", - "__experimentalRole": "local" + "role": "local" }, "href": { "type": "string" diff --git a/packages/block-library/src/file/index.php b/packages/block-library/src/file/index.php index 85cc840201da59..8ea668d56d8545 100644 --- a/packages/block-library/src/file/index.php +++ b/packages/block-library/src/file/index.php @@ -19,18 +19,7 @@ function render_block_core_file( $attributes, $content ) { // If it's interactive, enqueue the script module and add the directives. if ( ! empty( $attributes['displayPreview'] ) ) { - $suffix = wp_scripts_get_suffix(); - if ( defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN ) { - $module_url = gutenberg_url( '/build-module/block-library/file/view.min.js' ); - } - - wp_register_script_module( - '@wordpress/block-library/file', - isset( $module_url ) ? $module_url : includes_url( "blocks/file/view{$suffix}.js" ), - array( '@wordpress/interactivity' ), - defined( 'GUTENBERG_VERSION' ) ? GUTENBERG_VERSION : get_bloginfo( 'version' ) - ); - wp_enqueue_script_module( '@wordpress/block-library/file' ); + wp_enqueue_script_module( '@wordpress/block-library/file/view' ); $processor = new WP_HTML_Tag_Processor( $content ); $processor->next_tag(); diff --git a/packages/block-library/src/form-input/block.json b/packages/block-library/src/form-input/block.json index 53aa0be6744cb9..386c90ac207ad4 100644 --- a/packages/block-library/src/form-input/block.json +++ b/packages/block-library/src/form-input/block.json @@ -23,7 +23,7 @@ "default": "Label", "selector": ".wp-block-form-input__label-content", "source": "rich-text", - "__experimentalRole": "content" + "role": "content" }, "inlineLabel": { "type": "boolean", @@ -41,7 +41,7 @@ "selector": ".wp-block-form-input__input", "source": "attribute", "attribute": "placeholder", - "__experimentalRole": "content" + "role": "content" }, "value": { "type": "string", diff --git a/packages/block-library/src/form-input/deprecated.js b/packages/block-library/src/form-input/deprecated.js index 451cc704a42d55..d974cca387a188 100644 --- a/packages/block-library/src/form-input/deprecated.js +++ b/packages/block-library/src/form-input/deprecated.js @@ -41,7 +41,7 @@ const v2 = { default: 'Label', selector: '.wp-block-form-input__label-content', source: 'html', - __experimentalRole: 'content', + role: 'content', }, inlineLabel: { type: 'boolean', @@ -59,7 +59,7 @@ const v2 = { selector: '.wp-block-form-input__input', source: 'attribute', attribute: 'placeholder', - __experimentalRole: 'content', + role: 'content', }, value: { type: 'string', @@ -155,7 +155,7 @@ const v1 = { default: 'Label', selector: '.wp-block-form-input__label-content', source: 'html', - __experimentalRole: 'content', + role: 'content', }, inlineLabel: { type: 'boolean', @@ -173,7 +173,7 @@ const v1 = { selector: '.wp-block-form-input__input', source: 'attribute', attribute: 'placeholder', - __experimentalRole: 'content', + role: 'content', }, value: { type: 'string', diff --git a/packages/block-library/src/group/editor.scss b/packages/block-library/src/group/editor.scss index 11beecbab0eb68..739a9cd0cf852e 100644 --- a/packages/block-library/src/group/editor.scss +++ b/packages/block-library/src/group/editor.scss @@ -39,9 +39,9 @@ &::after { content: ""; display: flex; - flex: 1 0 $grid-unit-60; + flex: 1 0 $button-size-next-default-40px; pointer-events: none; - min-height: $grid-unit-60 - $border-width - $border-width; + min-height: $button-size-next-default-40px - $border-width - $border-width; border: $border-width dashed currentColor; } diff --git a/packages/block-library/src/heading/block.json b/packages/block-library/src/heading/block.json index 6e43a18cfba452..2276bcbbb50172 100644 --- a/packages/block-library/src/heading/block.json +++ b/packages/block-library/src/heading/block.json @@ -15,7 +15,7 @@ "type": "rich-text", "source": "rich-text", "selector": "h1,h2,h3,h4,h5,h6", - "__experimentalRole": "content" + "role": "content" }, "level": { "type": "number", diff --git a/packages/block-library/src/heading/deprecated.js b/packages/block-library/src/heading/deprecated.js index a97415712bf07c..76b175ac44fc40 100644 --- a/packages/block-library/src/heading/deprecated.js +++ b/packages/block-library/src/heading/deprecated.js @@ -259,7 +259,7 @@ const v5 = { source: 'html', selector: 'h1,h2,h3,h4,h5,h6', default: '', - __experimentalRole: 'content', + role: 'content', }, level: { type: 'number', diff --git a/packages/block-library/src/image/block.json b/packages/block-library/src/image/block.json index 6417879164a22b..f441a6e893290b 100644 --- a/packages/block-library/src/image/block.json +++ b/packages/block-library/src/image/block.json @@ -11,14 +11,14 @@ "attributes": { "blob": { "type": "string", - "__experimentalRole": "local" + "role": "local" }, "url": { "type": "string", "source": "attribute", "selector": "img", "attribute": "src", - "__experimentalRole": "content" + "role": "content" }, "alt": { "type": "string", @@ -26,13 +26,13 @@ "selector": "img", "attribute": "alt", "default": "", - "__experimentalRole": "content" + "role": "content" }, "caption": { "type": "rich-text", "source": "rich-text", "selector": "figcaption", - "__experimentalRole": "content" + "role": "content" }, "lightbox": { "type": "object", @@ -45,14 +45,14 @@ "source": "attribute", "selector": "img", "attribute": "title", - "__experimentalRole": "content" + "role": "content" }, "href": { "type": "string", "source": "attribute", "selector": "figure > a", "attribute": "href", - "__experimentalRole": "content" + "role": "content" }, "rel": { "type": "string", @@ -68,7 +68,7 @@ }, "id": { "type": "number", - "__experimentalRole": "content" + "role": "content" }, "width": { "type": "string" diff --git a/packages/block-library/src/image/deprecated.js b/packages/block-library/src/image/deprecated.js index 135463a377131f..6c1db75c5e2aa5 100644 --- a/packages/block-library/src/image/deprecated.js +++ b/packages/block-library/src/image/deprecated.js @@ -559,7 +559,7 @@ const v6 = { source: 'attribute', selector: 'img', attribute: 'src', - __experimentalRole: 'content', + role: 'content', }, alt: { type: 'string', @@ -567,27 +567,27 @@ const v6 = { selector: 'img', attribute: 'alt', default: '', - __experimentalRole: 'content', + role: 'content', }, caption: { type: 'string', source: 'html', selector: 'figcaption', - __experimentalRole: 'content', + role: 'content', }, title: { type: 'string', source: 'attribute', selector: 'img', attribute: 'title', - __experimentalRole: 'content', + role: 'content', }, href: { type: 'string', source: 'attribute', selector: 'figure > a', attribute: 'href', - __experimentalRole: 'content', + role: 'content', }, rel: { type: 'string', @@ -603,7 +603,7 @@ const v6 = { }, id: { type: 'number', - __experimentalRole: 'content', + role: 'content', }, width: { type: 'number', @@ -762,7 +762,7 @@ const v7 = { source: 'attribute', selector: 'img', attribute: 'src', - __experimentalRole: 'content', + role: 'content', }, alt: { type: 'string', @@ -770,27 +770,27 @@ const v7 = { selector: 'img', attribute: 'alt', default: '', - __experimentalRole: 'content', + role: 'content', }, caption: { type: 'string', source: 'html', selector: 'figcaption', - __experimentalRole: 'content', + role: 'content', }, title: { type: 'string', source: 'attribute', selector: 'img', attribute: 'title', - __experimentalRole: 'content', + role: 'content', }, href: { type: 'string', source: 'attribute', selector: 'figure > a', attribute: 'href', - __experimentalRole: 'content', + role: 'content', }, rel: { type: 'string', @@ -806,7 +806,7 @@ const v7 = { }, id: { type: 'number', - __experimentalRole: 'content', + role: 'content', }, width: { type: 'number', @@ -962,7 +962,7 @@ const v8 = { source: 'attribute', selector: 'img', attribute: 'src', - __experimentalRole: 'content', + role: 'content', }, alt: { type: 'string', @@ -970,27 +970,27 @@ const v8 = { selector: 'img', attribute: 'alt', default: '', - __experimentalRole: 'content', + role: 'content', }, caption: { type: 'string', source: 'html', selector: 'figcaption', - __experimentalRole: 'content', + role: 'content', }, title: { type: 'string', source: 'attribute', selector: 'img', attribute: 'title', - __experimentalRole: 'content', + role: 'content', }, href: { type: 'string', source: 'attribute', selector: 'figure > a', attribute: 'href', - __experimentalRole: 'content', + role: 'content', }, rel: { type: 'string', @@ -1006,7 +1006,7 @@ const v8 = { }, id: { type: 'number', - __experimentalRole: 'content', + role: 'content', }, width: { type: 'string', diff --git a/packages/block-library/src/image/edit.js b/packages/block-library/src/image/edit.js index d44dc73abfd855..360c4b8e6127b8 100644 --- a/packages/block-library/src/image/edit.js +++ b/packages/block-library/src/image/edit.js @@ -7,7 +7,7 @@ import clsx from 'clsx'; * WordPress dependencies */ import { isBlobURL, createBlobURL } from '@wordpress/blob'; -import { store as blocksStore, createBlock } from '@wordpress/blocks'; +import { createBlock, getBlockBindingsSource } from '@wordpress/blocks'; import { Placeholder } from '@wordpress/components'; import { useDispatch, useSelect } from '@wordpress/data'; import { @@ -28,7 +28,6 @@ import { useResizeObserver } from '@wordpress/compose'; /** * Internal dependencies */ -import { unlock } from '../lock-unlock'; import { useUploadMediaFromBlobURL } from '../utils/hooks'; import Image from './image'; import { isValidFileType } from './utils'; @@ -372,9 +371,9 @@ export function ImageEdit( { return {}; } - const blockBindingsSource = unlock( - select( blocksStore ) - ).getBlockBindingsSource( metadata?.bindings?.url?.source ); + const blockBindingsSource = getBlockBindingsSource( + metadata?.bindings?.url?.source + ); return { lockUrlControls: diff --git a/packages/block-library/src/image/image.js b/packages/block-library/src/image/image.js index 1673d36e463d5a..89bf31f92664b9 100644 --- a/packages/block-library/src/image/image.js +++ b/packages/block-library/src/image/image.js @@ -34,7 +34,7 @@ import { useEffect, useMemo, useState, useRef } from '@wordpress/element'; import { __, _x, sprintf, isRTL } from '@wordpress/i18n'; import { DOWN } from '@wordpress/keycodes'; import { getFilename } from '@wordpress/url'; -import { switchToBlockType, store as blocksStore } from '@wordpress/blocks'; +import { getBlockBindingsSource, switchToBlockType } from '@wordpress/blocks'; import { crop, overlayText, upload } from '@wordpress/icons'; import { store as noticesStore } from '@wordpress/notices'; import { store as coreStore } from '@wordpress/core-data'; @@ -476,7 +476,6 @@ export default function Image( { if ( ! isSingleSelected ) { return {}; } - const { getBlockBindingsSource } = unlock( select( blocksStore ) ); const { url: urlBinding, alt: altBinding, diff --git a/packages/block-library/src/image/index.php b/packages/block-library/src/image/index.php index abbb03c0952452..1a5fae7ce9cbb7 100644 --- a/packages/block-library/src/image/index.php +++ b/packages/block-library/src/image/index.php @@ -24,7 +24,7 @@ function render_block_core_image( $attributes, $content, $block ) { $p = new WP_HTML_Tag_Processor( $content ); - if ( ! $p->next_tag( 'img' ) || null === $p->get_attribute( 'src' ) ) { + if ( ! $p->next_tag( 'img' ) || ! $p->get_attribute( 'src' ) ) { return ''; } @@ -70,19 +70,7 @@ function render_block_core_image( $attributes, $content, $block ) { isset( $lightbox_settings['enabled'] ) && true === $lightbox_settings['enabled'] ) { - $suffix = wp_scripts_get_suffix(); - if ( defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN ) { - $module_url = gutenberg_url( '/build-module/block-library/image/view.min.js' ); - } - - wp_register_script_module( - '@wordpress/block-library/image', - isset( $module_url ) ? $module_url : includes_url( "blocks/image/view{$suffix}.js" ), - array( '@wordpress/interactivity' ), - defined( 'GUTENBERG_VERSION' ) ? GUTENBERG_VERSION : get_bloginfo( 'version' ) - ); - - wp_enqueue_script_module( '@wordpress/block-library/image' ); + wp_enqueue_script_module( '@wordpress/block-library/image/view' ); /* * This render needs to happen in a filter with priority 15 to ensure that diff --git a/packages/block-library/src/latest-posts/edit.js b/packages/block-library/src/latest-posts/edit.js index ab792ae0e353db..c7801118e456cd 100644 --- a/packages/block-library/src/latest-posts/edit.js +++ b/packages/block-library/src/latest-posts/edit.js @@ -242,7 +242,6 @@ export default function LatestPostsEdit( { attributes, setAttributes } ) { /> { displayPostContent && ( { sprintf( - /* translators: byline. %s: current author. */ + /* translators: byline. %s: author. */ __( 'by %s' ), currentAuthor.name ) } diff --git a/packages/block-library/src/latest-posts/editor.scss b/packages/block-library/src/latest-posts/editor.scss index aaab74cc0ef2d6..fe901480974461 100644 --- a/packages/block-library/src/latest-posts/editor.scss +++ b/packages/block-library/src/latest-posts/editor.scss @@ -18,14 +18,3 @@ padding-left: 0; } } - -// Apply the same styles that would be applied to -// ".block-editor-block-inspector .components-base-control" -// (see packages/block-editor/src/components/block-inspector/style.scss) -.wp-block-latest-posts__post-content-radio { - margin-bottom: #{ $grid-unit-30 }; - - &:last-child { - margin-bottom: $grid-unit-10; - } -} diff --git a/packages/block-library/src/latest-posts/index.php b/packages/block-library/src/latest-posts/index.php index 85c7b58737a1cf..f8fd8ea27bc453 100644 --- a/packages/block-library/src/latest-posts/index.php +++ b/packages/block-library/src/latest-posts/index.php @@ -123,7 +123,7 @@ function render_block_core_latest_posts( $attributes ) { if ( isset( $attributes['displayAuthor'] ) && $attributes['displayAuthor'] ) { $author_display_name = get_the_author_meta( 'display_name', $post->post_author ); - /* translators: byline. %s: current author. */ + /* translators: byline. %s: author. */ $byline = sprintf( __( 'by %s' ), $author_display_name ); if ( ! empty( $author_display_name ) ) { diff --git a/packages/block-library/src/list-item/block.json b/packages/block-library/src/list-item/block.json index a4bf2351d97509..6eb30cfe6d0af0 100644 --- a/packages/block-library/src/list-item/block.json +++ b/packages/block-library/src/list-item/block.json @@ -16,7 +16,7 @@ "type": "rich-text", "source": "rich-text", "selector": "li", - "__experimentalRole": "content" + "role": "content" } }, "supports": { diff --git a/packages/block-library/src/list/block.json b/packages/block-library/src/list/block.json index ea07a0eb542df3..4a86def8d687b4 100644 --- a/packages/block-library/src/list/block.json +++ b/packages/block-library/src/list/block.json @@ -12,7 +12,7 @@ "ordered": { "type": "boolean", "default": false, - "__experimentalRole": "content" + "role": "content" }, "values": { "type": "string", @@ -21,7 +21,7 @@ "multiline": "li", "__unstableMultilineWrapperTags": [ "ol", "ul" ], "default": "", - "__experimentalRole": "content" + "role": "content" }, "type": { "type": "string" diff --git a/packages/block-library/src/list/deprecated.js b/packages/block-library/src/list/deprecated.js index edb04dff27c904..13804b7040ed46 100644 --- a/packages/block-library/src/list/deprecated.js +++ b/packages/block-library/src/list/deprecated.js @@ -14,7 +14,7 @@ const v0 = { ordered: { type: 'boolean', default: false, - __experimentalRole: 'content', + role: 'content', }, values: { type: 'string', @@ -23,7 +23,7 @@ const v0 = { multiline: 'li', __unstableMultilineWrapperTags: [ 'ol', 'ul' ], default: '', - __experimentalRole: 'content', + role: 'content', }, type: { type: 'string', @@ -74,7 +74,7 @@ const v1 = { ordered: { type: 'boolean', default: false, - __experimentalRole: 'content', + role: 'content', }, values: { type: 'string', @@ -83,7 +83,7 @@ const v1 = { multiline: 'li', __unstableMultilineWrapperTags: [ 'ol', 'ul' ], default: '', - __experimentalRole: 'content', + role: 'content', }, type: { type: 'string', @@ -144,7 +144,7 @@ const v2 = { ordered: { type: 'boolean', default: false, - __experimentalRole: 'content', + role: 'content', }, values: { type: 'string', @@ -153,7 +153,7 @@ const v2 = { multiline: 'li', __unstableMultilineWrapperTags: [ 'ol', 'ul' ], default: '', - __experimentalRole: 'content', + role: 'content', }, type: { type: 'string', @@ -225,7 +225,7 @@ const v3 = { ordered: { type: 'boolean', default: false, - __experimentalRole: 'content', + role: 'content', }, values: { type: 'string', @@ -234,7 +234,7 @@ const v3 = { multiline: 'li', __unstableMultilineWrapperTags: [ 'ol', 'ul' ], default: '', - __experimentalRole: 'content', + role: 'content', }, type: { type: 'string', diff --git a/packages/block-library/src/media-text/block.json b/packages/block-library/src/media-text/block.json index 42384c0c4478e0..0c2cfc4a14995a 100644 --- a/packages/block-library/src/media-text/block.json +++ b/packages/block-library/src/media-text/block.json @@ -18,7 +18,7 @@ "selector": "figure img", "attribute": "alt", "default": "", - "__experimentalRole": "content" + "role": "content" }, "mediaPosition": { "type": "string", @@ -26,14 +26,14 @@ }, "mediaId": { "type": "number", - "__experimentalRole": "content" + "role": "content" }, "mediaUrl": { "type": "string", "source": "attribute", "selector": "figure video,figure img", "attribute": "src", - "__experimentalRole": "content" + "role": "content" }, "mediaLink": { "type": "string" @@ -52,7 +52,7 @@ "source": "attribute", "selector": "figure a", "attribute": "href", - "__experimentalRole": "content" + "role": "content" }, "rel": { "type": "string", @@ -68,7 +68,7 @@ }, "mediaType": { "type": "string", - "__experimentalRole": "content" + "role": "content" }, "mediaWidth": { "type": "number", diff --git a/packages/block-library/src/media-text/deprecated.js b/packages/block-library/src/media-text/deprecated.js index 54c6f863311ffe..24f239a41ed295 100644 --- a/packages/block-library/src/media-text/deprecated.js +++ b/packages/block-library/src/media-text/deprecated.js @@ -172,29 +172,29 @@ const v6Attributes = { selector: 'figure img', attribute: 'alt', default: '', - __experimentalRole: 'content', + role: 'content', }, mediaId: { type: 'number', - __experimentalRole: 'content', + role: 'content', }, mediaUrl: { type: 'string', source: 'attribute', selector: 'figure video,figure img', attribute: 'src', - __experimentalRole: 'content', + role: 'content', }, href: { type: 'string', source: 'attribute', selector: 'figure a', attribute: 'href', - __experimentalRole: 'content', + role: 'content', }, mediaType: { type: 'string', - __experimentalRole: 'content', + role: 'content', }, }; diff --git a/packages/block-library/src/navigation/edit/index.js b/packages/block-library/src/navigation/edit/index.js index ae7dd60bd0c5ba..d961f0d8fc93b9 100644 --- a/packages/block-library/src/navigation/edit/index.js +++ b/packages/block-library/src/navigation/edit/index.js @@ -73,6 +73,33 @@ import AccessibleDescription from './accessible-description'; import AccessibleMenuDescription from './accessible-menu-description'; import { unlock } from '../../lock-unlock'; +function useResponsiveMenu( navRef ) { + const [ isResponsiveMenuOpen, setResponsiveMenuVisibility ] = + useState( false ); + + useEffect( () => { + if ( ! navRef.current ) { + return; + } + + const htmlElement = navRef.current.ownerDocument.documentElement; + + // Add a `has-modal-open` class to the when the responsive + // menu is open. This reproduces the same behavior of the frontend. + if ( isResponsiveMenuOpen ) { + htmlElement.classList.add( 'has-modal-open' ); + } else { + htmlElement.classList.remove( 'has-modal-open' ); + } + + return () => { + htmlElement?.classList.remove( 'has-modal-open' ); + }; + }, [ navRef, isResponsiveMenuOpen ] ); + + return [ isResponsiveMenuOpen, setResponsiveMenuVisibility ]; +} + function ColorTools( { textColor, setTextColor, @@ -284,8 +311,10 @@ function Navigation( { __unstableMarkNextChangeAsNotPersistent, } = useDispatch( blockEditorStore ); + const navRef = useRef(); + const [ isResponsiveMenuOpen, setResponsiveMenuVisibility ] = - useState( false ); + useResponsiveMenu( navRef ); const [ overlayMenuPreview, setOverlayMenuPreview ] = useState( false ); @@ -367,8 +396,6 @@ function Navigation( { __unstableMarkNextChangeAsNotPersistent, ] ); - const navRef = useRef(); - // The standard HTML5 tag for the block wrapper. const TagName = 'nav'; diff --git a/packages/block-library/src/navigation/edit/menu-inspector-controls.js b/packages/block-library/src/navigation/edit/menu-inspector-controls.js index 8f6c2e47fe7759..a2c6078d9fabe2 100644 --- a/packages/block-library/src/navigation/edit/menu-inspector-controls.js +++ b/packages/block-library/src/navigation/edit/menu-inspector-controls.js @@ -26,8 +26,8 @@ import LeafMoreMenu from './leaf-more-menu'; import { updateAttributes } from '../../navigation-link/update-attributes'; import { LinkUI } from '../../navigation-link/link-ui'; -/* translators: %s: The name of a menu. */ -const actionLabel = __( "Switch to '%s'" ); +const actionLabel = + /* translators: %s: The name of a menu. */ __( "Switch to '%s'" ); const BLOCKS_WITH_LINK_UI_SUPPORT = [ 'core/navigation-link', 'core/navigation-submenu', diff --git a/packages/block-library/src/navigation/edit/navigation-menu-selector.js b/packages/block-library/src/navigation/edit/navigation-menu-selector.js index 007220b1789131..dceabf063b26e8 100644 --- a/packages/block-library/src/navigation/edit/navigation-menu-selector.js +++ b/packages/block-library/src/navigation/edit/navigation-menu-selector.js @@ -21,7 +21,7 @@ import useNavigationEntities from '../use-navigation-entities'; function buildMenuLabel( title, id, status ) { if ( ! title ) { - /* translators: %s is the index of the menu in the list of menus. */ + /* translators: %s: the index of the menu in the list of menus. */ return sprintf( __( '(no title %s)' ), id ); } @@ -30,7 +30,7 @@ function buildMenuLabel( title, id, status ) { } return sprintf( - // translators: %1s: title of the menu; %2s: status of the menu (draft, pending, etc.). + // translators: 1: title of the menu. 2: status of the menu (draft, pending, etc.). __( '%1$s (%2$s)' ), decodeEntities( title ), status diff --git a/packages/block-library/src/navigation/editor.scss b/packages/block-library/src/navigation/editor.scss index 1d37186e4ae98b..2f61a19be2b799 100644 --- a/packages/block-library/src/navigation/editor.scss +++ b/packages/block-library/src/navigation/editor.scss @@ -203,6 +203,7 @@ $color-control-label-height: 20px; .wp-block-navigation .wp-block + .block-list-appender .block-editor-button-block-appender { background-color: $gray-900; color: $white; + height: $button-size-small; // This needs specificity to override an inherited padding. // That source padding in turn has high specificity to protect diff --git a/packages/block-library/src/navigation/index.php b/packages/block-library/src/navigation/index.php index ec72b03b6906f0..fa9bb5a56f8012 100644 --- a/packages/block-library/src/navigation/index.php +++ b/packages/block-library/src/navigation/index.php @@ -622,18 +622,7 @@ private static function get_nav_element_directives( $is_interactive ) { */ private static function handle_view_script_module_loading( $attributes, $block, $inner_blocks ) { if ( static::is_interactive( $attributes, $inner_blocks ) ) { - $suffix = wp_scripts_get_suffix(); - if ( defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN ) { - $module_url = gutenberg_url( '/build-module/block-library/navigation/view.min.js' ); - } - - wp_register_script_module( - '@wordpress/block-library/navigation', - isset( $module_url ) ? $module_url : includes_url( "blocks/navigation/view{$suffix}.js" ), - array( '@wordpress/interactivity' ), - defined( 'GUTENBERG_VERSION' ) ? GUTENBERG_VERSION : get_bloginfo( 'version' ) - ); - wp_enqueue_script_module( '@wordpress/block-library/navigation' ); + wp_enqueue_script_module( '@wordpress/block-library/navigation/view' ); } } @@ -1510,9 +1499,15 @@ function block_core_navigation_mock_parsed_block( $inner_blocks, $post ) { */ function block_core_navigation_insert_hooked_blocks( $inner_blocks, $post ) { $mock_navigation_block = block_core_navigation_mock_parsed_block( $inner_blocks, $post ); - $hooked_blocks = get_hooked_blocks(); - $before_block_visitor = null; - $after_block_visitor = null; + + if ( function_exists( 'apply_block_hooks_to_content' ) ) { + $mock_navigation_block_markup = serialize_block( $mock_navigation_block ); + return apply_block_hooks_to_content( $mock_navigation_block_markup, $post, 'insert_hooked_blocks' ); + } + + $hooked_blocks = get_hooked_blocks(); + $before_block_visitor = null; + $after_block_visitor = null; if ( ! empty( $hooked_blocks ) || has_filter( 'hooked_block_types' ) ) { $before_block_visitor = make_before_block_visitor( $hooked_blocks, $post, 'insert_hooked_blocks' ); diff --git a/packages/block-library/src/paragraph/block.json b/packages/block-library/src/paragraph/block.json index f16a7cf0411443..7e004019cbf282 100644 --- a/packages/block-library/src/paragraph/block.json +++ b/packages/block-library/src/paragraph/block.json @@ -15,7 +15,7 @@ "type": "rich-text", "source": "rich-text", "selector": "p", - "__experimentalRole": "content" + "role": "content" }, "dropCap": { "type": "boolean", diff --git a/packages/block-library/src/post-content/edit.js b/packages/block-library/src/post-content/edit.js index 041a3b4fa02f61..582165303c0d05 100644 --- a/packages/block-library/src/post-content/edit.js +++ b/packages/block-library/src/post-content/edit.js @@ -8,19 +8,24 @@ import { RecursionProvider, useHasRecursion, Warning, + __experimentalUseBlockPreview as useBlockPreview, } from '@wordpress/block-editor'; +import { parse } from '@wordpress/blocks'; import { useEntityProp, useEntityBlockEditor, store as coreStore, } from '@wordpress/core-data'; import { useSelect } from '@wordpress/data'; +import { useMemo } from '@wordpress/element'; + /** * Internal dependencies */ import { useCanEditEntity } from '../utils/hooks'; function ReadOnlyContent( { + parentLayout, layoutClassNames, userCanEdit, postType, @@ -33,7 +38,28 @@ function ReadOnlyContent( { postId ); const blockProps = useBlockProps( { className: layoutClassNames } ); - return content?.protected && ! userCanEdit ? ( + const blocks = useMemo( () => { + return content?.raw ? parse( content.raw ) : []; + }, [ content?.raw ] ); + const blockPreviewProps = useBlockPreview( { + blocks, + props: blockProps, + layout: parentLayout, + } ); + + if ( userCanEdit ) { + /* + * Rendering the block preview using the raw content blocks allows for + * block support styles to be generated and applied by the editor. + * + * The preview using the raw blocks can only be presented to users with + * edit permissions for the post to prevent potential exposure of private + * block content. + */ + return
; + } + + return content?.protected ? (
{ __( 'This content is password protected.' ) }
@@ -96,6 +122,7 @@ function Content( props ) { ) : ( ) : ( diff --git a/packages/block-library/src/post-content/style.scss b/packages/block-library/src/post-content/style.scss index 96e27d04679d44..1d3e210e1210d7 100644 --- a/packages/block-library/src/post-content/style.scss +++ b/packages/block-library/src/post-content/style.scss @@ -1,5 +1,3 @@ -.wp-block-post-content::after { - content: ""; - display: table; - clear: both; +.wp-block-post-content { + display: flow-root; } diff --git a/packages/block-library/src/post-featured-image/edit.js b/packages/block-library/src/post-featured-image/edit.js index dff34379e4f3b4..95441a5a55cfd0 100644 --- a/packages/block-library/src/post-featured-image/edit.js +++ b/packages/block-library/src/post-featured-image/edit.js @@ -207,7 +207,7 @@ export default function PostFeaturedImageEdit( { label={ postType?.labels.singular_name ? sprintf( - // translators: %s: Name of the post type e.g: "Page". + // translators: %s: Name of the post type e.g: "post". __( 'Link to %s' ), postType.labels.singular_name ) diff --git a/packages/block-library/src/post-template/index.php b/packages/block-library/src/post-template/index.php index 64cdd156a54310..9126355c096a57 100644 --- a/packages/block-library/src/post-template/index.php +++ b/packages/block-library/src/post-template/index.php @@ -64,11 +64,6 @@ function render_block_core_post_template( $attributes, $content, $block ) { if ( in_the_loop() ) { $query = clone $wp_query; $query->rewind_posts(); - - // If in a single post of any post type, default to the 'post' post type. - if ( is_singular() ) { - query_posts( array( 'post_type' => 'post' ) ); - } } else { $query = $wp_query; } diff --git a/packages/block-library/src/post-time-to-read/edit.js b/packages/block-library/src/post-time-to-read/edit.js index abfdce6630a67d..0533b99d3fd9d7 100644 --- a/packages/block-library/src/post-time-to-read/edit.js +++ b/packages/block-library/src/post-time-to-read/edit.js @@ -71,8 +71,8 @@ function PostTimeToReadEdit( { attributes, setAttributes, context } ) { ); return sprintf( - /* translators: %d is the number of minutes the post will take to read. */ - _n( '%d minute', '%d minutes', minutesToRead ), + /* translators: %s: the number of minutes to read the post. */ + _n( '%s minute', '%s minutes', minutesToRead ), minutesToRead ); }, [ contentStructure, blocks ] ); diff --git a/packages/block-library/src/post-time-to-read/index.php b/packages/block-library/src/post-time-to-read/index.php index f7abb1903f04df..eac79d11450b7d 100644 --- a/packages/block-library/src/post-time-to-read/index.php +++ b/packages/block-library/src/post-time-to-read/index.php @@ -32,7 +32,7 @@ function render_block_core_post_time_to_read( $attributes, $content, $block ) { $minutes_to_read = max( 1, (int) round( wp_word_count( $content, $word_count_type ) / $average_reading_rate ) ); $minutes_to_read_string = sprintf( - /* translators: %s is the number of minutes the post will take to read. */ + /* translators: %s: the number of minutes to read the post. */ _n( '%s minute', '%s minutes', $minutes_to_read ), $minutes_to_read ); diff --git a/packages/block-library/src/preformatted/block.json b/packages/block-library/src/preformatted/block.json index a1726ee8b0d43c..c25b8ce37093a5 100644 --- a/packages/block-library/src/preformatted/block.json +++ b/packages/block-library/src/preformatted/block.json @@ -12,7 +12,7 @@ "source": "rich-text", "selector": "pre", "__unstablePreserveWhiteSpace": true, - "__experimentalRole": "content" + "role": "content" } }, "supports": { diff --git a/packages/block-library/src/pullquote/block.json b/packages/block-library/src/pullquote/block.json index 0935f9759668d5..271bba74d0252a 100644 --- a/packages/block-library/src/pullquote/block.json +++ b/packages/block-library/src/pullquote/block.json @@ -11,13 +11,13 @@ "type": "rich-text", "source": "rich-text", "selector": "p", - "__experimentalRole": "content" + "role": "content" }, "citation": { "type": "rich-text", "source": "rich-text", "selector": "cite", - "__experimentalRole": "content" + "role": "content" }, "textAlign": { "type": "string" diff --git a/packages/block-library/src/pullquote/deprecated.js b/packages/block-library/src/pullquote/deprecated.js index 6e6f49da91c6a3..18e47997550782 100644 --- a/packages/block-library/src/pullquote/deprecated.js +++ b/packages/block-library/src/pullquote/deprecated.js @@ -75,14 +75,14 @@ const v5 = { source: 'html', selector: 'blockquote', multiline: 'p', - __experimentalRole: 'content', + role: 'content', }, citation: { type: 'string', source: 'html', selector: 'cite', default: '', - __experimentalRole: 'content', + role: 'content', }, textAlign: { type: 'string', diff --git a/packages/block-library/src/query-title/edit.js b/packages/block-library/src/query-title/edit.js index 845809a9300137..21d23081837cdf 100644 --- a/packages/block-library/src/query-title/edit.js +++ b/packages/block-library/src/query-title/edit.js @@ -15,7 +15,7 @@ import { HeadingLevelDropdown, } from '@wordpress/block-editor'; import { ToggleControl, PanelBody } from '@wordpress/components'; -import { __, sprintf } from '@wordpress/i18n'; +import { __, _x, sprintf } from '@wordpress/i18n'; /** * Internal dependencies @@ -60,7 +60,7 @@ export default function QueryTitleEdit( { if ( archiveNameLabel ) { title = sprintf( /* translators: 1: Archive type title e.g: "Category", 2: Label of the archive e.g: "Shoes" */ - __( '%1$s: %2$s' ), + _x( '%1$s: %2$s', 'archive label' ), archiveTypeLabel, archiveNameLabel ); diff --git a/packages/block-library/src/query/edit/inspector-controls/format-controls.js b/packages/block-library/src/query/edit/inspector-controls/format-controls.js index d26fd9d81ce6f7..15c95f3bbba2e2 100644 --- a/packages/block-library/src/query/edit/inspector-controls/format-controls.js +++ b/packages/block-library/src/query/edit/inspector-controls/format-controls.js @@ -68,7 +68,7 @@ export default function FormatControls( { onChange, query: { format } } ) { .filter( Boolean ); const suggestions = formats - .filter( ( item ) => ! format.includes( item.value ) ) + .filter( ( item ) => ! normalizedFormats.includes( item.value ) ) .map( ( item ) => item.label ); return ( diff --git a/packages/block-library/src/query/edit/inspector-controls/index.js b/packages/block-library/src/query/edit/inspector-controls/index.js index 4085128e9aef1a..02b96491fb6e1b 100644 --- a/packages/block-library/src/query/edit/inspector-controls/index.js +++ b/packages/block-library/src/query/edit/inspector-controls/index.js @@ -123,21 +123,18 @@ export default function QueryInspectorControls( props ) { const showInheritControl = isTemplate && isControlAllowed( allowedControls, 'inherit' ); const showPostTypeControl = - ( ! inherit && isControlAllowed( allowedControls, 'postType' ) ) || - ! isTemplate; + ! inherit && isControlAllowed( allowedControls, 'postType' ); const postTypeControlLabel = __( 'Post type' ); const postTypeControlHelp = __( 'Select the type of content to display: posts, pages, or custom post types.' ); const showColumnsControl = false; const showOrderControl = - ( ! inherit && isControlAllowed( allowedControls, 'order' ) ) || - ! isTemplate; + ! inherit && isControlAllowed( allowedControls, 'order' ); const showStickyControl = - ( ! inherit && - showSticky && - isControlAllowed( allowedControls, 'sticky' ) ) || - ( showSticky && ! isTemplate ); + ! inherit && + showSticky && + isControlAllowed( allowedControls, 'sticky' ); const showSettingsPanel = showInheritControl || showPostTypeControl || diff --git a/packages/block-library/src/query/edit/inspector-controls/order-control.js b/packages/block-library/src/query/edit/inspector-controls/order-control.js index 2f6fa0e589d473..23802f844359d7 100644 --- a/packages/block-library/src/query/edit/inspector-controls/order-control.js +++ b/packages/block-library/src/query/edit/inspector-controls/order-control.js @@ -14,12 +14,12 @@ const orderOptions = [ value: 'date/asc', }, { - /* translators: label for ordering posts by title in ascending order */ + /* translators: Label for ordering posts by title in ascending order. */ label: __( 'A → Z' ), value: 'title/asc', }, { - /* translators: label for ordering posts by title in descending order */ + /* translators: Label for ordering posts by title in descending order. */ label: __( 'Z → A' ), value: 'title/desc', }, diff --git a/packages/block-library/src/query/index.php b/packages/block-library/src/query/index.php index d10db26529854e..043f351e11d7f1 100644 --- a/packages/block-library/src/query/index.php +++ b/packages/block-library/src/query/index.php @@ -24,27 +24,7 @@ function render_block_core_query( $attributes, $content, $block ) { // Enqueue the script module and add the necessary directives if the block is // interactive. if ( $is_interactive ) { - $suffix = wp_scripts_get_suffix(); - if ( defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN ) { - $module_url = gutenberg_url( '/build-module/block-library/query/view.min.js' ); - } - - wp_register_script_module( - '@wordpress/block-library/query', - isset( $module_url ) ? $module_url : includes_url( "blocks/query/view{$suffix}.js" ), - array( - array( - 'id' => '@wordpress/interactivity', - 'import' => 'static', - ), - array( - 'id' => '@wordpress/interactivity-router', - 'import' => 'dynamic', - ), - ), - defined( 'GUTENBERG_VERSION' ) ? GUTENBERG_VERSION : get_bloginfo( 'version' ) - ); - wp_enqueue_script_module( '@wordpress/block-library/query' ); + wp_enqueue_script_module( '@wordpress/block-library/query/view' ); $p = new WP_HTML_Tag_Processor( $content ); if ( $p->next_tag() ) { diff --git a/packages/block-library/src/quote/block.json b/packages/block-library/src/quote/block.json index 0f9ec97422f64b..2ae37f9f36f766 100644 --- a/packages/block-library/src/quote/block.json +++ b/packages/block-library/src/quote/block.json @@ -14,13 +14,13 @@ "selector": "blockquote", "multiline": "p", "default": "", - "__experimentalRole": "content" + "role": "content" }, "citation": { "type": "rich-text", "source": "rich-text", "selector": "cite", - "__experimentalRole": "content" + "role": "content" }, "textAlign": { "type": "string" diff --git a/packages/block-library/src/quote/deprecated.js b/packages/block-library/src/quote/deprecated.js index 77098b6e753139..4d3efd28e3a22c 100644 --- a/packages/block-library/src/quote/deprecated.js +++ b/packages/block-library/src/quote/deprecated.js @@ -70,14 +70,14 @@ const v4 = { selector: 'blockquote', multiline: 'p', default: '', - __experimentalRole: 'content', + role: 'content', }, citation: { type: 'string', source: 'html', selector: 'cite', default: '', - __experimentalRole: 'content', + role: 'content', }, align: { type: 'string', @@ -138,14 +138,14 @@ const v3 = { selector: 'blockquote', multiline: 'p', default: '', - __experimentalRole: 'content', + role: 'content', }, citation: { type: 'string', source: 'html', selector: 'cite', default: '', - __experimentalRole: 'content', + role: 'content', }, align: { type: 'string', diff --git a/packages/block-library/src/rss/index.php b/packages/block-library/src/rss/index.php index 3e8231d259e700..32885863402d5e 100644 --- a/packages/block-library/src/rss/index.php +++ b/packages/block-library/src/rss/index.php @@ -62,7 +62,7 @@ function render_block_core_rss( $attributes ) { if ( is_object( $author ) ) { $author = $author->get_name(); $author = '' . sprintf( - /* translators: %s: the author. */ + /* translators: byline. %s: author. */ __( 'by %s' ), esc_html( strip_tags( $author ) ) ) . ''; diff --git a/packages/block-library/src/search/block.json b/packages/block-library/src/search/block.json index dac4c6b488a97e..c5af5a29d21beb 100644 --- a/packages/block-library/src/search/block.json +++ b/packages/block-library/src/search/block.json @@ -10,7 +10,7 @@ "attributes": { "label": { "type": "string", - "__experimentalRole": "content" + "role": "content" }, "showLabel": { "type": "boolean", @@ -19,7 +19,7 @@ "placeholder": { "type": "string", "default": "", - "__experimentalRole": "content" + "role": "content" }, "width": { "type": "number" @@ -29,7 +29,7 @@ }, "buttonText": { "type": "string", - "__experimentalRole": "content" + "role": "content" }, "buttonPosition": { "type": "string", diff --git a/packages/block-library/src/search/edit.js b/packages/block-library/src/search/edit.js index e2f3bb3999e42c..b68914893c0e84 100644 --- a/packages/block-library/src/search/edit.js +++ b/packages/block-library/src/search/edit.js @@ -23,14 +23,14 @@ import { useEffect, useRef } from '@wordpress/element'; import { ToolbarDropdownMenu, ToolbarGroup, - Button, - ButtonGroup, ToolbarButton, ResizableBox, PanelBody, __experimentalVStack as VStack, __experimentalUseCustomUnits as useCustomUnits, __experimentalUnitControl as UnitControl, + __experimentalToggleGroupControl as ToggleGroupControl, + __experimentalToggleGroupControlOption as ToggleGroupControlOption, } from '@wordpress/components'; import { useInstanceId } from '@wordpress/compose'; import { Icon, search } from '@wordpress/icons'; @@ -58,6 +58,7 @@ import { // Used to calculate border radius adjustment to avoid "fat" corners when // button is placed inside wrapper. const DEFAULT_INNER_PADDING = '4px'; +const PERCENTAGE_WIDTHS = [ 25, 50, 75, 100 ]; export default function SearchEdit( { className, @@ -424,13 +425,12 @@ export default function SearchEdit( { } step={ 1 } onChange={ ( newWidth ) => { - const filteredWidth = - widthUnit === '%' && - parseInt( newWidth, 10 ) > 100 - ? 100 - : newWidth; + const parsedNewWidth = + newWidth === '' + ? undefined + : parseInt( newWidth, 10 ); setAttributes( { - width: parseInt( filteredWidth, 10 ), + width: parsedNewWidth, } ); } } onUnitChange={ ( newUnit ) => { @@ -446,33 +446,35 @@ export default function SearchEdit( { value={ `${ width }${ widthUnit }` } units={ units } /> - { + setAttributes( { + width: newWidth, + widthUnit: '%', + } ); + } } + isBlock + __next40pxDefaultSize + __nextHasNoMarginBottom > - { [ 25, 50, 75, 100 ].map( ( widthValue ) => { + { PERCENTAGE_WIDTHS.map( ( widthValue ) => { return ( - + value={ widthValue } + label={ `${ widthValue }%` } + /> ); } ) } - + @@ -566,7 +568,11 @@ export default function SearchEdit( { set_attribute( 'data-wp-bind--aria-hidden', '!context.isSearchInputVisible' ); $input->set_attribute( 'data-wp-bind--tabindex', 'state.tabindex' ); diff --git a/packages/block-library/src/template-part/edit/index.js b/packages/block-library/src/template-part/edit/index.js index 951f875143b0f9..ae941c977e14fd 100644 --- a/packages/block-library/src/template-part/edit/index.js +++ b/packages/block-library/src/template-part/edit/index.js @@ -214,7 +214,7 @@ export default function TemplatePartEdit( { { sprintf( - /* translators: %s: Template part slug */ + /* translators: %s: Template part slug. */ __( 'Template part has been deleted or is unavailable: %s' ), diff --git a/packages/block-library/src/verse/block.json b/packages/block-library/src/verse/block.json index 387ff3dfe17123..81cccd72965b1a 100644 --- a/packages/block-library/src/verse/block.json +++ b/packages/block-library/src/verse/block.json @@ -13,7 +13,7 @@ "source": "rich-text", "selector": "pre", "__unstablePreserveWhiteSpace": true, - "__experimentalRole": "content" + "role": "content" }, "textAlign": { "type": "string" diff --git a/packages/block-library/src/verse/deprecated.js b/packages/block-library/src/verse/deprecated.js index 7e3c96bc80cd98..bd4edc46738c5c 100644 --- a/packages/block-library/src/verse/deprecated.js +++ b/packages/block-library/src/verse/deprecated.js @@ -46,7 +46,7 @@ const v2 = { selector: 'pre', default: '', __unstablePreserveWhiteSpace: true, - __experimentalRole: 'content', + role: 'content', }, textAlign: { type: 'string', diff --git a/packages/block-library/src/video/block.json b/packages/block-library/src/video/block.json index 1d3dc75961e8f1..d2dcd95365c3b5 100644 --- a/packages/block-library/src/video/block.json +++ b/packages/block-library/src/video/block.json @@ -18,7 +18,7 @@ "type": "rich-text", "source": "rich-text", "selector": "figcaption", - "__experimentalRole": "content" + "role": "content" }, "controls": { "type": "boolean", @@ -29,7 +29,7 @@ }, "id": { "type": "number", - "__experimentalRole": "content" + "role": "content" }, "loop": { "type": "boolean", @@ -58,14 +58,14 @@ }, "blob": { "type": "string", - "__experimentalRole": "local" + "role": "local" }, "src": { "type": "string", "source": "attribute", "selector": "video", "attribute": "src", - "__experimentalRole": "content" + "role": "content" }, "playsInline": { "type": "boolean", @@ -74,7 +74,7 @@ "attribute": "playsinline" }, "tracks": { - "__experimentalRole": "content", + "role": "content", "type": "array", "items": { "type": "object" diff --git a/packages/block-library/src/video/tracks-editor.js b/packages/block-library/src/video/tracks-editor.js index d01e365985bc96..10fb241a9ecb30 100644 --- a/packages/block-library/src/video/tracks-editor.js +++ b/packages/block-library/src/video/tracks-editor.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { __, sprintf } from '@wordpress/i18n'; +import { __, _x, sprintf } from '@wordpress/i18n'; import { NavigableMenu, MenuItem, @@ -63,7 +63,7 @@ function TrackList( { tracks, onEditPress } ) { onClick={ () => onEditPress( index ) } aria-label={ sprintf( /* translators: %s: Label of the video text track e.g: "French subtitles" */ - __( 'Edit %s' ), + _x( 'Edit %s', 'text tracks' ), track.label ) } > diff --git a/packages/block-serialization-default-parser/CHANGELOG.md b/packages/block-serialization-default-parser/CHANGELOG.md index 9dd0bd70309764..47e1db147942ff 100644 --- a/packages/block-serialization-default-parser/CHANGELOG.md +++ b/packages/block-serialization-default-parser/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 5.8.0 (2024-09-19) + ## 5.7.0 (2024-09-05) ## 5.6.0 (2024-08-21) diff --git a/packages/block-serialization-default-parser/package.json b/packages/block-serialization-default-parser/package.json index cac606bd7bd1ee..db2ddd7eecf408 100644 --- a/packages/block-serialization-default-parser/package.json +++ b/packages/block-serialization-default-parser/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/block-serialization-default-parser", - "version": "5.7.0", + "version": "5.8.1", "description": "Block serialization specification parser for WordPress posts.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/block-serialization-spec-parser/CHANGELOG.md b/packages/block-serialization-spec-parser/CHANGELOG.md index b8d02b5348e1b6..173ce09db51e2b 100644 --- a/packages/block-serialization-spec-parser/CHANGELOG.md +++ b/packages/block-serialization-spec-parser/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 5.8.0 (2024-09-19) + ## 5.7.0 (2024-09-05) ## 5.6.0 (2024-08-21) diff --git a/packages/block-serialization-spec-parser/package.json b/packages/block-serialization-spec-parser/package.json index d48d1846e79095..2c6bc7c4e21e5d 100644 --- a/packages/block-serialization-spec-parser/package.json +++ b/packages/block-serialization-spec-parser/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/block-serialization-spec-parser", - "version": "5.7.0", + "version": "5.8.1", "description": "Block serialization specification parser for WordPress posts.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/blocks/CHANGELOG.md b/packages/blocks/CHANGELOG.md index 0755d5170d1b32..838983c17233ea 100644 --- a/packages/blocks/CHANGELOG.md +++ b/packages/blocks/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 13.8.0 (2024-09-19) + ## 13.7.0 (2024-09-05) ## 13.6.0 (2024-08-21) diff --git a/packages/blocks/README.md b/packages/blocks/README.md index d724f986b0ca81..c5a754f88d4929 100644 --- a/packages/blocks/README.md +++ b/packages/blocks/README.md @@ -102,6 +102,39 @@ _Returns_ - `Object`: All block attributes. +### getBlockAttributesNamesByRole + +Filter block attributes by `role` and return their names. + +_Parameters_ + +- _name_ `string`: Block attribute's name. +- _role_ `string`: The role of a block attribute. + +_Returns_ + +- `string[]`: The attribute names that have the provided role. + +### getBlockBindingsSource + +Returns a registered block bindings source by its name. + +_Parameters_ + +- _name_ `string`: Block bindings source name. + +_Returns_ + +- `?Object`: Block bindings source. + +### getBlockBindingsSources + +Returns all registered block bindings sources. + +_Returns_ + +- `Array`: Block bindings sources. + ### getBlockContent Given a block object, returns the Block's Inner HTML markup. @@ -479,6 +512,36 @@ _Returns_ - `Array`: A list of blocks. +### registerBlockBindingsSource + +Registers a new block bindings source with an object defining its behavior. Once registered, the source is available to be connected to the supported block attributes. + +_Usage_ + +```js +import { _x } from '@wordpress/i18n'; +import { registerBlockBindingsSource } from '@wordpress/blocks'; + +registerBlockBindingsSource( { + name: 'plugin/my-custom-source', + label: _x( 'My Custom Source', 'block bindings source' ), + usesContext: [ 'postType' ], + getValues: getSourceValues, + setValues: updateMyCustomValuesInBatch, + canUserEditValue: () => true, +} ); +``` + +_Parameters_ + +- _source_ `Object`: Properties of the source to be registered. +- _source.name_ `string`: The unique and machine-readable name. +- _source.label_ `[string]`: Human-readable label. Optional when it is defined in the server. +- _source.usesContext_ `[Array]`: Optional array of context needed by the source only in the editor. +- _source.getValues_ `[Function]`: Optional function to get the values from the source. +- _source.setValues_ `[Function]`: Optional function to update multiple values connected to the source. +- _source.canUserEditValue_ `[Function]`: Optional function to determine if the user can edit the value. + ### registerBlockCollection Registers a new block collection to group blocks in the same namespace in the inserter. @@ -780,6 +843,22 @@ _Returns_ - `Array`: Updated Block list. +### unregisterBlockBindingsSource + +Unregisters a block bindings source by providing its name. + +_Usage_ + +```js +import { unregisterBlockBindingsSource } from '@wordpress/blocks'; + +unregisterBlockBindingsSource( 'plugin/my-custom-source' ); +``` + +_Parameters_ + +- _name_ `string`: The name of the block bindings source to unregister. + ### unregisterBlockStyle Unregisters a block style for the given block. diff --git a/packages/blocks/package.json b/packages/blocks/package.json index f371ecc087af41..87bb27d5020ee0 100644 --- a/packages/blocks/package.json +++ b/packages/blocks/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/blocks", - "version": "13.7.0", + "version": "13.8.5", "description": "Block API for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/blocks/src/api/index.js b/packages/blocks/src/api/index.js index 803467cb2187e2..0b38b8e29e68a0 100644 --- a/packages/blocks/src/api/index.js +++ b/packages/blocks/src/api/index.js @@ -2,12 +2,7 @@ * Internal dependencies */ import { lock } from '../lock-unlock'; -import { - registerBlockBindingsSource, - unregisterBlockBindingsSource, - getBlockBindingsSource, - getBlockBindingsSources, -} from './registration'; +import { isUnmodifiedBlockContent } from './utils'; // The blocktype is the most important concept within the block API. It defines // all aspects of the block configuration and its interfaces, including `edit` @@ -146,6 +141,10 @@ export { unregisterBlockStyle, registerBlockVariation, unregisterBlockVariation, + registerBlockBindingsSource, + unregisterBlockBindingsSource, + getBlockBindingsSource, + getBlockBindingsSources, } from './registration'; export { isUnmodifiedBlock, @@ -155,6 +154,7 @@ export { getBlockLabel as __experimentalGetBlockLabel, getAccessibleBlockLabel as __experimentalGetAccessibleBlockLabel, __experimentalSanitizeBlockAttributes, + getBlockAttributesNamesByRole, __experimentalGetBlockAttributesNamesByRole, } from './utils'; @@ -177,9 +177,4 @@ export { } from './constants'; export const privateApis = {}; -lock( privateApis, { - registerBlockBindingsSource, - unregisterBlockBindingsSource, - getBlockBindingsSource, - getBlockBindingsSources, -} ); +lock( privateApis, { isUnmodifiedBlockContent } ); diff --git a/packages/blocks/src/api/registration.js b/packages/blocks/src/api/registration.js index b0f5ae350759f0..e2e3bd66e7a9b2 100644 --- a/packages/blocks/src/api/registration.js +++ b/packages/blocks/src/api/registration.js @@ -769,12 +769,11 @@ export const unregisterBlockVariation = ( blockName, variationName ) => { * * @param {Object} source Properties of the source to be registered. * @param {string} source.name The unique and machine-readable name. - * @param {string} [source.label] Human-readable label. - * @param {Array} [source.usesContext] Array of context needed by the source only in the editor. - * @param {Function} [source.getValues] Function to get the values from the source. - * @param {Function} [source.setValues] Function to update multiple values connected to the source. - * @param {Function} [source.canUserEditValue] Function to determine if the user can edit the value. - * @param {Function} [source.getFieldsList] Function to get the lists of fields to expose in the connections panel. + * @param {string} [source.label] Human-readable label. Optional when it is defined in the server. + * @param {Array} [source.usesContext] Optional array of context needed by the source only in the editor. + * @param {Function} [source.getValues] Optional function to get the values from the source. + * @param {Function} [source.setValues] Optional function to update multiple values connected to the source. + * @param {Function} [source.canUserEditValue] Optional function to determine if the user can edit the value. * * @example * ```js @@ -784,8 +783,9 @@ export const unregisterBlockVariation = ( blockName, variationName ) => { * registerBlockBindingsSource( { * name: 'plugin/my-custom-source', * label: _x( 'My Custom Source', 'block bindings source' ), - * getValues: () => getSourceValues(), - * setValues: () => updateMyCustomValuesInBatch(), + * usesContext: [ 'postType' ], + * getValues: getSourceValues, + * setValues: updateMyCustomValuesInBatch, * canUserEditValue: () => true, * } ); * ``` @@ -807,13 +807,16 @@ export const registerBlockBindingsSource = ( source ) => { /* * Check if the source has been already registered on the client. - * If the `getValues` property is defined, it could be assumed the source is already registered. + * If any property expected to be "client-only" is defined, return a warning. */ - if ( existingSource?.getValues ) { - warning( - 'Block bindings source "' + name + '" is already registered.' - ); - return; + const serverProps = [ 'label', 'usesContext' ]; + for ( const prop in existingSource ) { + if ( ! serverProps.includes( prop ) && existingSource[ prop ] ) { + warning( + 'Block bindings source "' + name + '" is already registered.' + ); + return; + } } // Check the `name` property is correct. @@ -849,14 +852,6 @@ export const registerBlockBindingsSource = ( source ) => { } // Check the `label` property is correct. - if ( label && existingSource?.label ) { - warning( - 'Block bindings "' + - name + - '" source label is already defined in the server.' - ); - return; - } if ( ! label && ! existingSource?.label ) { warning( 'Block bindings source must contain a label.' ); @@ -868,6 +863,10 @@ export const registerBlockBindingsSource = ( source ) => { return; } + if ( label && existingSource?.label && label !== existingSource?.label ) { + warning( 'Block bindings "' + name + '" source label was overriden.' ); + } + // Check the `usesContext` property is correct. if ( usesContext && ! Array.isArray( usesContext ) ) { warning( 'Block bindings source usesContext must be an array.' ); @@ -903,7 +902,7 @@ export const registerBlockBindingsSource = ( source ) => { }; /** - * Unregisters a block bindings source + * Unregisters a block bindings source by providing its name. * * @param {string} name The name of the block bindings source to unregister. * @@ -924,7 +923,7 @@ export function unregisterBlockBindingsSource( name ) { } /** - * Returns a registered block bindings source. + * Returns a registered block bindings source by its name. * * @param {string} name Block bindings source name. * diff --git a/packages/blocks/src/api/serializer.js b/packages/blocks/src/api/serializer.js index 2e7246ce9584a9..f1fb28e9d9a361 100644 --- a/packages/blocks/src/api/serializer.js +++ b/packages/blocks/src/api/serializer.js @@ -10,6 +10,7 @@ import { import { hasFilter, applyFilters } from '@wordpress/hooks'; import isShallowEqual from '@wordpress/is-shallow-equal'; import { removep } from '@wordpress/autop'; +import deprecated from '@wordpress/deprecated'; /** * Internal dependencies @@ -238,7 +239,17 @@ export function getCommentAttributes( blockType, attributes ) { } // Ignore all local attributes + if ( attributeSchema.role === 'local' ) { + return accumulator; + } + if ( attributeSchema.__experimentalRole === 'local' ) { + deprecated( '__experimentalRole attribute', { + since: '6.7', + version: '6.8', + alternative: 'role attribute', + hint: `Check the block.json of the ${ blockType?.name } block.`, + } ); return accumulator; } diff --git a/packages/blocks/src/api/test/registration.js b/packages/blocks/src/api/test/registration.js index 483949af4fe5f2..73f310bbf04dc7 100644 --- a/packages/blocks/src/api/test/registration.js +++ b/packages/blocks/src/api/test/registration.js @@ -1512,22 +1512,38 @@ describe( 'blocks', () => { expect( getBlockBindingsSource( 'core/testing' ) ).toBeUndefined(); } ); - it( 'should not override label from the server', () => { - // Bootstrap source from the server. - unlock( - dispatch( blocksStore ) - ).addBootstrappedBlockBindingsSource( { - name: 'core/server', + it( 'should override label from the server', () => { + // Simulate bootstrap source from the server. + registerBlockBindingsSource( { + name: 'core/testing', label: 'Server label', } ); // Override the source with a different label in the client. registerBlockBindingsSource( { - name: 'core/server', + name: 'core/testing', label: 'Client label', } ); expect( console ).toHaveWarnedWith( - 'Block bindings "core/server" source label is already defined in the server.' + 'Block bindings "core/testing" source label was overriden.' ); + const source = getBlockBindingsSource( 'core/testing' ); + unregisterBlockBindingsSource( 'core/testing' ); + expect( source.label ).toEqual( 'Client label' ); + } ); + + it( 'should keep label from the server when not defined in the client', () => { + // Simulate bootstrap source from the server. + registerBlockBindingsSource( { + name: 'core/testing', + label: 'Server label', + } ); + // Override the source with a different label in the client. + registerBlockBindingsSource( { + name: 'core/testing', + } ); + const source = getBlockBindingsSource( 'core/testing' ); + unregisterBlockBindingsSource( 'core/testing' ); + expect( source.label ).toEqual( 'Server label' ); } ); // Check the `usesContext` array is correct. @@ -1543,10 +1559,8 @@ describe( 'blocks', () => { } ); it( 'should add usesContext when only defined in the server', () => { - // Bootstrap source from the server. - unlock( - dispatch( blocksStore ) - ).addBootstrappedBlockBindingsSource( { + // Simulate bootstrap source from the server. + registerBlockBindingsSource( { name: 'core/testing', label: 'testing', usesContext: [ 'postId', 'postType' ], @@ -1562,10 +1576,8 @@ describe( 'blocks', () => { } ); it( 'should add usesContext when only defined in the client', () => { - // Bootstrap source from the server. - unlock( - dispatch( blocksStore ) - ).addBootstrappedBlockBindingsSource( { + // Simulate bootstrap source from the server. + registerBlockBindingsSource( { name: 'core/testing', label: 'testing', } ); @@ -1581,10 +1593,8 @@ describe( 'blocks', () => { } ); it( 'should merge usesContext from server and client without duplicates', () => { - // Bootstrap source from the server. - unlock( - dispatch( blocksStore ) - ).addBootstrappedBlockBindingsSource( { + // Simulate bootstrap source from the server. + registerBlockBindingsSource( { name: 'core/testing', label: 'testing', usesContext: [ 'postId', 'postType' ], @@ -1705,42 +1715,6 @@ describe( 'blocks', () => { 'Block bindings source "core/test-source" is already registered.' ); } ); - - it( 'should correctly merge properties when bootstrap happens after registration', () => { - // Register source in the client. - const clientOnlyProperties = { - getValues: () => 'values', - setValues: () => 'new values', - canUserEditValue: () => true, - }; - registerBlockBindingsSource( { - name: 'core/custom-source', - label: 'Client Label', - usesContext: [ 'postId', 'postType' ], - ...clientOnlyProperties, - } ); - - // Bootstrap source from the server. - unlock( - dispatch( blocksStore ) - ).addBootstrappedBlockBindingsSource( { - name: 'core/custom-source', - label: 'Server Label', - usesContext: [ 'postId', 'serverContext' ], - } ); - - // Check that the bootstrap values prevail and the client properties are still there. - expect( getBlockBindingsSource( 'core/custom-source' ) ).toEqual( { - // Should use the server label. - label: 'Server Label', - // Should merge usesContext from server and client. - usesContext: [ 'postId', 'postType', 'serverContext' ], - // Should keep client properties. - ...clientOnlyProperties, - } ); - - unregisterBlockBindingsSource( 'core/custom-source' ); - } ); } ); describe( 'unregisterBlockBindingsSource', () => { diff --git a/packages/blocks/src/api/test/serializer.js b/packages/blocks/src/api/test/serializer.js index 7fed23041daaa6..3c1cbd6d1e74ff 100644 --- a/packages/blocks/src/api/test/serializer.js +++ b/packages/blocks/src/api/test/serializer.js @@ -155,7 +155,7 @@ describe( 'block serializer', () => { attributes: { blob: { type: 'string', - __experimentalRole: 'local', + role: 'local', }, url: { type: 'string', diff --git a/packages/blocks/src/api/test/utils.js b/packages/blocks/src/api/test/utils.js index 9bfef69c4c1428..ad76e89aafe5f0 100644 --- a/packages/blocks/src/api/test/utils.js +++ b/packages/blocks/src/api/test/utils.js @@ -13,7 +13,7 @@ import { getAccessibleBlockLabel, getBlockLabel, __experimentalSanitizeBlockAttributes, - __experimentalGetBlockAttributesNamesByRole, + getBlockAttributesNamesByRole, } from '../utils'; const noop = () => {}; @@ -309,7 +309,7 @@ describe( 'sanitizeBlockAttributes', () => { } ); } ); -describe( '__experimentalGetBlockAttributesNamesByRole', () => { +describe( 'getBlockAttributesNamesByRole', () => { beforeAll( () => { registerBlockType( 'core/test-block-1', { attributes: { @@ -318,15 +318,15 @@ describe( '__experimentalGetBlockAttributesNamesByRole', () => { }, content: { type: 'boolean', - __experimentalRole: 'content', + role: 'content', }, level: { type: 'number', - __experimentalRole: 'content', + role: 'content', }, color: { type: 'string', - __experimentalRole: 'other', + role: 'other', }, }, save: noop, @@ -357,42 +357,28 @@ describe( '__experimentalGetBlockAttributesNamesByRole', () => { ].forEach( unregisterBlockType ); } ); it( 'should return empty array if block has no attributes', () => { - expect( - __experimentalGetBlockAttributesNamesByRole( 'core/test-block-3' ) - ).toEqual( [] ); + expect( getBlockAttributesNamesByRole( 'core/test-block-3' ) ).toEqual( + [] + ); } ); it( 'should return all attribute names if no role is provided', () => { - expect( - __experimentalGetBlockAttributesNamesByRole( 'core/test-block-1' ) - ).toEqual( + expect( getBlockAttributesNamesByRole( 'core/test-block-1' ) ).toEqual( expect.arrayContaining( [ 'align', 'content', 'level', 'color' ] ) ); } ); it( 'should return proper results with existing attributes and provided role', () => { expect( - __experimentalGetBlockAttributesNamesByRole( - 'core/test-block-1', - 'content' - ) + getBlockAttributesNamesByRole( 'core/test-block-1', 'content' ) ).toEqual( expect.arrayContaining( [ 'content', 'level' ] ) ); expect( - __experimentalGetBlockAttributesNamesByRole( - 'core/test-block-1', - 'other' - ) + getBlockAttributesNamesByRole( 'core/test-block-1', 'other' ) ).toEqual( [ 'color' ] ); expect( - __experimentalGetBlockAttributesNamesByRole( - 'core/test-block-1', - 'not-exists' - ) + getBlockAttributesNamesByRole( 'core/test-block-1', 'not-exists' ) ).toEqual( [] ); // A block with no `role` in any attributes. expect( - __experimentalGetBlockAttributesNamesByRole( - 'core/test-block-2', - 'content' - ) + getBlockAttributesNamesByRole( 'core/test-block-2', 'content' ) ).toEqual( [] ); } ); } ); diff --git a/packages/blocks/src/api/utils.js b/packages/blocks/src/api/utils.js index a68937586f9273..7bace4ff84c29b 100644 --- a/packages/blocks/src/api/utils.js +++ b/packages/blocks/src/api/utils.js @@ -12,6 +12,7 @@ import { Component, isValidElement } from '@wordpress/element'; import { __, sprintf } from '@wordpress/i18n'; import { __unstableStripHTML as stripHTML } from '@wordpress/dom'; import { RichTextData } from '@wordpress/rich-text'; +import deprecated from '@wordpress/deprecated'; /** * Internal dependencies @@ -29,6 +30,30 @@ extend( [ namesPlugin, a11yPlugin ] ); */ const ICON_COLORS = [ '#191e23', '#f8f9f9' ]; +/** + * Determines whether the block's attribute is equal to the default attribute + * which means the attribute is unmodified. + * @param {Object} attributeDefinition The attribute's definition of the block type. + * @param {*} value The attribute's value. + * @return {boolean} Whether the attribute is unmodified. + */ +function isUnmodifiedAttribute( attributeDefinition, value ) { + // Every attribute that has a default must match the default. + if ( attributeDefinition.hasOwnProperty( 'default' ) ) { + return value === attributeDefinition.default; + } + + // The rich text type is a bit different from the rest because it + // has an implicit default value of an empty RichTextData instance, + // so check the length of the value. + if ( attributeDefinition.type === 'rich-text' ) { + return ! value?.length; + } + + // Every attribute that doesn't have a default should be undefined. + return value === undefined; +} + /** * Determines whether the block's attributes are equal to the default attributes * which means the block is unmodified. @@ -42,20 +67,7 @@ export function isUnmodifiedBlock( block ) { ( [ key, definition ] ) => { const value = block.attributes[ key ]; - // Every attribute that has a default must match the default. - if ( definition.hasOwnProperty( 'default' ) ) { - return value === definition.default; - } - - // The rich text type is a bit different from the rest because it - // has an implicit default value of an empty RichTextData instance, - // so check the length of the value. - if ( definition.type === 'rich-text' ) { - return ! value?.length; - } - - // Every attribute that doesn't have a default should be undefined. - return value === undefined; + return isUnmodifiedAttribute( definition, value ); } ); } @@ -72,6 +84,35 @@ export function isUnmodifiedDefaultBlock( block ) { return block.name === getDefaultBlockName() && isUnmodifiedBlock( block ); } +/** + * Determines whether the block content is unmodified. A block content is + * considered unmodified if all the attributes that have a role of 'content' + * are equal to the default attributes (or undefined). + * If the block does not have any attributes with a role of 'content', it + * will be considered unmodified if all the attributes are equal to the default + * attributes (or undefined). + * + * @param {WPBlock} block Block Object + * @return {boolean} Whether the block content is unmodified. + */ +export function isUnmodifiedBlockContent( block ) { + const contentAttributes = getBlockAttributesNamesByRole( + block.name, + 'content' + ); + + if ( contentAttributes.length === 0 ) { + return isUnmodifiedBlock( block ); + } + + return contentAttributes.every( ( key ) => { + const definition = getBlockType( block.name )?.attributes[ key ]; + const value = block.attributes[ key ]; + + return isUnmodifiedAttribute( definition, value ); + } ); +} + /** * Function that checks if the parameter is a valid icon. * @@ -332,7 +373,7 @@ export function __experimentalSanitizeBlockAttributes( name, attributes ) { * * @return {string[]} The attribute names that have the provided role. */ -export function __experimentalGetBlockAttributesNamesByRole( name, role ) { +export function getBlockAttributesNamesByRole( name, role ) { const attributes = getBlockType( name )?.attributes; if ( ! attributes ) { return []; @@ -341,12 +382,34 @@ export function __experimentalGetBlockAttributesNamesByRole( name, role ) { if ( ! role ) { return attributesNames; } - return attributesNames.filter( - ( attributeName ) => - attributes[ attributeName ]?.__experimentalRole === role - ); + + return attributesNames.filter( ( attributeName ) => { + const attribute = attributes[ attributeName ]; + if ( attribute?.role === role ) { + return true; + } + if ( attribute?.__experimentalRole === role ) { + deprecated( '__experimentalRole attribute', { + since: '6.7', + version: '6.8', + alternative: 'role attribute', + hint: `Check the block.json of the ${ name } block.`, + } ); + return true; + } + return false; + } ); } +export const __experimentalGetBlockAttributesNamesByRole = ( ...args ) => { + deprecated( '__experimentalGetBlockAttributesNamesByRole', { + since: '6.7', + version: '6.8', + alternative: 'getBlockAttributesNamesByRole', + } ); + return getBlockAttributesNamesByRole( ...args ); +}; + /** * Return a new object with the specified keys omitted. * diff --git a/packages/blocks/src/store/private-actions.js b/packages/blocks/src/store/private-actions.js index 02f8506b3c3bf8..bfefe56773d77a 100644 --- a/packages/blocks/src/store/private-actions.js +++ b/packages/blocks/src/store/private-actions.js @@ -70,17 +70,3 @@ export function removeBlockBindingsSource( name ) { name, }; } - -/** - * Add bootstrapped block bindings sources, usually initialized from the server. - * - * @param {string} source Name of the source to bootstrap. - */ -export function addBootstrappedBlockBindingsSource( source ) { - return { - type: 'ADD_BOOTSTRAPPED_BLOCK_BINDINGS_SOURCE', - name: source.name, - label: source.label, - usesContext: source.usesContext, - }; -} diff --git a/packages/blocks/src/store/private-selectors.js b/packages/blocks/src/store/private-selectors.js index 4cded8268ae97c..d5665323859e40 100644 --- a/packages/blocks/src/store/private-selectors.js +++ b/packages/blocks/src/store/private-selectors.js @@ -2,6 +2,7 @@ * WordPress dependencies */ import { createSelector } from '@wordpress/data'; +import deprecated from '@wordpress/deprecated'; /** * Internal dependencies @@ -209,3 +210,36 @@ export function getAllBlockBindingsSources( state ) { export function getBlockBindingsSource( state, sourceName ) { return state.blockBindingsSources[ sourceName ]; } + +/** + * Determines if any of the block type's attributes have + * the content role attribute. + * + * @param {Object} state Data state. + * @param {string} blockTypeName Block type name. + * @return {boolean} Whether block type has content role attribute. + */ +export const hasContentRoleAttribute = ( state, blockTypeName ) => { + const blockType = getBlockType( state, blockTypeName ); + if ( ! blockType ) { + return false; + } + + return Object.values( blockType.attributes ).some( + ( { role, __experimentalRole } ) => { + if ( role === 'content' ) { + return true; + } + if ( __experimentalRole === 'content' ) { + deprecated( '__experimentalRole attribute', { + since: '6.7', + version: '6.8', + alternative: 'role attribute', + hint: `Check the block.json of the ${ blockTypeName } block.`, + } ); + return true; + } + return false; + } + ); +}; diff --git a/packages/blocks/src/store/reducer.js b/packages/blocks/src/store/reducer.js index fbcec7a619cf63..ac652b91890319 100644 --- a/packages/blocks/src/store/reducer.js +++ b/packages/blocks/src/store/reducer.js @@ -393,35 +393,27 @@ function getMergedUsesContext( existingUsesContext = [], newUsesContext = [] ) { export function blockBindingsSources( state = {}, action ) { switch ( action.type ) { case 'ADD_BLOCK_BINDINGS_SOURCE': + // Only open this API in Gutenberg and for `core/post-meta` for the moment. + let getFieldsList; + if ( globalThis.IS_GUTENBERG_PLUGIN ) { + getFieldsList = action.getFieldsList; + } else if ( action.name === 'core/post-meta' ) { + getFieldsList = action.getFieldsList; + } return { ...state, [ action.name ]: { - // Don't override the label if it's already set. - label: state[ action.name ]?.label || action.label, + label: action.label || state[ action.name ]?.label, usesContext: getMergedUsesContext( state[ action.name ]?.usesContext, action.usesContext ), getValues: action.getValues, setValues: action.setValues, - canUserEditValue: action.canUserEditValue, - getFieldsList: action.getFieldsList, - }, - }; - case 'ADD_BOOTSTRAPPED_BLOCK_BINDINGS_SOURCE': - return { - ...state, - [ action.name ]: { - /* - * Keep the exisitng properties in case the source has been registered - * in the client before bootstrapping. - */ - ...state[ action.name ], - label: action.label, - usesContext: getMergedUsesContext( - state[ action.name ]?.usesContext, - action.usesContext - ), + // Only set `canUserEditValue` if `setValues` is also defined. + canUserEditValue: + action.setValues && action.canUserEditValue, + getFieldsList, }, }; case 'REMOVE_BLOCK_BINDINGS_SOURCE': diff --git a/packages/blocks/src/store/selectors.js b/packages/blocks/src/store/selectors.js index e97048e92b0c07..79e88073ba20de 100644 --- a/packages/blocks/src/store/selectors.js +++ b/packages/blocks/src/store/selectors.js @@ -8,11 +8,13 @@ import removeAccents from 'remove-accents'; */ import { createSelector } from '@wordpress/data'; import { RichTextData } from '@wordpress/rich-text'; +import deprecated from '@wordpress/deprecated'; /** * Internal dependencies */ import { getValueFromObjectPath, matchesAttributes } from './utils'; +import { hasContentRoleAttribute as privateHasContentRoleAttribute } from './private-selectors'; /** @typedef {import('../api/registration').WPBlockVariation} WPBlockVariation */ /** @typedef {import('../api/registration').WPBlockVariationScope} WPBlockVariationScope */ @@ -822,23 +824,11 @@ export const hasChildBlocksWithInserterSupport = ( state, blockName ) => { } ); }; -/** - * DO-NOT-USE in production. - * This selector is created for internal/experimental only usage and may be - * removed anytime without any warning, causing breakage on any plugin or theme invoking it. - */ -export const __experimentalHasContentRoleAttribute = createSelector( - ( state, blockTypeName ) => { - const blockType = getBlockType( state, blockTypeName ); - if ( ! blockType ) { - return false; - } - - return Object.entries( blockType.attributes ).some( - ( [ , { __experimentalRole } ] ) => __experimentalRole === 'content' - ); - }, - ( state, blockTypeName ) => [ - state.blockTypes[ blockTypeName ]?.attributes, - ] -); +export const __experimentalHasContentRoleAttribute = ( ...args ) => { + deprecated( '__experimentalHasContentRoleAttribute', { + since: '6.7', + version: '6.8', + hint: 'This is a private selector.', + } ); + return privateHasContentRoleAttribute( ...args ); +}; diff --git a/packages/browserslist-config/CHANGELOG.md b/packages/browserslist-config/CHANGELOG.md index b8ee58633b7ef2..cbe4763ea32706 100644 --- a/packages/browserslist-config/CHANGELOG.md +++ b/packages/browserslist-config/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 6.8.0 (2024-09-19) + ## 6.7.0 (2024-09-05) ## 6.6.0 (2024-08-21) diff --git a/packages/browserslist-config/package.json b/packages/browserslist-config/package.json index c51d7318170c4e..0ddacd1a3f0dc7 100644 --- a/packages/browserslist-config/package.json +++ b/packages/browserslist-config/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/browserslist-config", - "version": "6.7.0", + "version": "6.8.1", "description": "WordPress Browserslist shared configuration.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/commands/CHANGELOG.md b/packages/commands/CHANGELOG.md index bde7767a0f61f6..b8f45c79470040 100644 --- a/packages/commands/CHANGELOG.md +++ b/packages/commands/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 1.8.0 (2024-09-19) + ## 1.7.0 (2024-09-05) ## 1.6.0 (2024-08-21) diff --git a/packages/commands/package.json b/packages/commands/package.json index 88b9b86f4b3aeb..f4dce48310b2c7 100644 --- a/packages/commands/package.json +++ b/packages/commands/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/commands", - "version": "1.7.0", + "version": "1.8.10", "description": "Handles the commands menu.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 4dd18914a7acb5..258e33c86b2744 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -4,6 +4,28 @@ ### Bug Fixes +- `ToggleGroupControl`: Don't set value on focus after a reset ([#66151](https://github.com/WordPress/gutenberg/pull/66151)). + +## 28.8.6 (2024-10-14) + +### Bug Fixes + +- `ToolsPanel`: atomic one-step state update when (un)registering panels ([#65564](https://github.com/WordPress/gutenberg/pull/65564)). +- `Navigator`: fix `isInitial` logic ([#65527](https://github.com/WordPress/gutenberg/pull/65527)). +- `ToggleGroupControl`: Fix arrow key navigation in RTL ([#65735](https://github.com/WordPress/gutenberg/pull/65735)). +- `Composite`: fix legacy support for the store prop ([#65821](https://github.com/WordPress/gutenberg/pull/65821)). +- `Composite`: make items tabbable if active element gets removed ([#65720](https://github.com/WordPress/gutenberg/pull/65720)). +- `DatePicker`: Use compact button size. ([#65653](https://github.com/WordPress/gutenberg/pull/65653)). +- `DropZone`: fix class names on drop ([#65798](https://github.com/WordPress/gutenberg/pull/65798)). +- `Tooltip`: add `aria-describedby` to the anchor only if not redundant ([#65989](https://github.com/WordPress/gutenberg/pull/65989)). +- `PaletteEdit`: dedupe palette element slugs ([#65772](https://github.com/WordPress/gutenberg/pull/65772)). +- `ToggleGroupControl`: Don't autoselect option on first group focus ([#65892](https://github.com/WordPress/gutenberg/pull/65892)). +- `Guide`: Update finish button to use the new default size ([#65680](https://github.com/WordPress/gutenberg/pull/65680)). + +## 28.8.0 (2024-09-19) + +### Bug Fixes + - `Tabs`: restore vertical indicator ([#65385](https://github.com/WordPress/gutenberg/pull/65385)). - `Tabs`: indicator positioning under RTL direction ([#64926](https://github.com/WordPress/gutenberg/pull/64926)). - `Popover`: Update `toolbar` variant radius to match block toolbar ([#65263](https://github.com/WordPress/gutenberg/pull/65263)). diff --git a/packages/components/package.json b/packages/components/package.json index 8337d6dd8857f4..d90b3c0098a422 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/components", - "version": "28.7.0", + "version": "28.8.10", "description": "UI components for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/components/src/border-control/border-control-dropdown/component.tsx b/packages/components/src/border-control/border-control-dropdown/component.tsx index b2951054e624e7..68db095b080989 100644 --- a/packages/components/src/border-control/border-control-dropdown/component.tsx +++ b/packages/components/src/border-control/border-control-dropdown/component.tsx @@ -76,15 +76,19 @@ const getToggleAriaLabel = ( const ariaLabelValue = getAriaLabelColorValue( colorObject.color ); return style ? sprintf( - // translators: %1$s: The name of the color e.g. "vivid red". %2$s: The color's hex code e.g.: "#f00:". %3$s: The current border style selection e.g. "solid". - 'Border color and style picker. The currently selected color is called "%1$s" and has a value of "%2$s". The currently selected style is "%3$s".', + // translators: 1: The name of the color e.g. "vivid red". 2: The color's hex code e.g.: "#f00:". 3: The current border style selection e.g. "solid". + __( + 'Border color and style picker. The currently selected color is called "%1$s" and has a value of "%2$s". The currently selected style is "%3$s".' + ), colorObject.name, ariaLabelValue, style ) : sprintf( - // translators: %1$s: The name of the color e.g. "vivid red". %2$s: The color's hex code e.g.: "#f00:". - 'Border color and style picker. The currently selected color is called "%1$s" and has a value of "%2$s".', + // translators: 1: The name of the color e.g. "vivid red". 2: The color's hex code e.g.: "#f00:". + __( + 'Border color and style picker. The currently selected color is called "%1$s" and has a value of "%2$s".' + ), colorObject.name, ariaLabelValue ); @@ -94,14 +98,18 @@ const getToggleAriaLabel = ( const ariaLabelValue = getAriaLabelColorValue( colorValue ); return style ? sprintf( - // translators: %1$s: The color's hex code e.g.: "#f00:". %2$s: The current border style selection e.g. "solid". - 'Border color and style picker. The currently selected color has a value of "%1$s". The currently selected style is "%2$s".', + // translators: 1: The color's hex code e.g.: "#f00:". 2: The current border style selection e.g. "solid". + __( + 'Border color and style picker. The currently selected color has a value of "%1$s". The currently selected style is "%2$s".' + ), ariaLabelValue, style ) : sprintf( - // translators: %1$s: The color's hex code e.g: "#f00". - 'Border color and style picker. The currently selected color has a value of "%1$s".', + // translators: %s: The color's hex code e.g: "#f00". + __( + 'Border color and style picker. The currently selected color has a value of "%s".' + ), ariaLabelValue ); } @@ -111,8 +119,10 @@ const getToggleAriaLabel = ( if ( colorObject ) { return sprintf( - // translators: %1$s: The name of the color e.g. "vivid red". %2$s: The color's hex code e.g: "#f00". - 'Border color picker. The currently selected color is called "%1$s" and has a value of "%2$s".', + // translators: 1: The name of the color e.g. "vivid red". 2: The color's hex code e.g: "#f00". + __( + 'Border color picker. The currently selected color is called "%1$s" and has a value of "%2$s".' + ), colorObject.name, getAriaLabelColorValue( colorObject.color ) ); @@ -120,8 +130,10 @@ const getToggleAriaLabel = ( if ( colorValue ) { return sprintf( - // translators: %1$s: The color's hex code e.g: "#f00". - 'Border color picker. The currently selected color has a value of "%1$s".', + // translators: %s: The color's hex code e.g: "#f00". + __( + 'Border color picker. The currently selected color has a value of "%s".' + ), getAriaLabelColorValue( colorValue ) ); } diff --git a/packages/components/src/color-palette/index.tsx b/packages/components/src/color-palette/index.tsx index c744faee9b0c3f..a65508d8278c5f 100644 --- a/packages/components/src/color-palette/index.tsx +++ b/packages/components/src/color-palette/index.tsx @@ -233,7 +233,7 @@ function UnforwardedColorPalette( const displayValue = value?.replace( /^var\((.+)\)$/, '$1' ); const customColorAccessibleLabel = !! displayValue ? sprintf( - // translators: %1$s: The name of the color e.g: "vivid red". %2$s: The color's hex code e.g: "#f00". + // translators: 1: The name of the color e.g: "vivid red". 2: The color's hex code e.g: "#f00". __( 'Custom color picker. The currently selected color is called "%1$s" and has a value of "%2$s".' ), diff --git a/packages/components/src/composite/group-label.tsx b/packages/components/src/composite/group-label.tsx index 17070dbb86bf81..7e3c6ffdc7759c 100644 --- a/packages/components/src/composite/group-label.tsx +++ b/packages/components/src/composite/group-label.tsx @@ -20,11 +20,13 @@ export const CompositeGroupLabel = forwardRef< WordPressComponentProps< CompositeGroupLabelProps, 'div', false > >( function CompositeGroupLabel( props, ref ) { const context = useCompositeContext(); + + // @ts-expect-error The store prop is undocumented and only used by the + // legacy compat layer. The `store` prop is documented, but its type is + // obfuscated to discourage its use outside of the component's internals. + const store = ( props.store ?? context.store ) as Ariakit.CompositeStore; + return ( - + ); } ); diff --git a/packages/components/src/composite/group.tsx b/packages/components/src/composite/group.tsx index ae21ca6f11dd92..bcfb47e684613d 100644 --- a/packages/components/src/composite/group.tsx +++ b/packages/components/src/composite/group.tsx @@ -20,11 +20,11 @@ export const CompositeGroup = forwardRef< WordPressComponentProps< CompositeGroupProps, 'div', false > >( function CompositeGroup( props, ref ) { const context = useCompositeContext(); - return ( - - ); + + // @ts-expect-error The store prop is undocumented and only used by the + // legacy compat layer. The `store` prop is documented, but its type is + // obfuscated to discourage its use outside of the component's internals. + const store = ( props.store ?? context.store ) as Ariakit.CompositeStore; + + return ; } ); diff --git a/packages/components/src/composite/hover.tsx b/packages/components/src/composite/hover.tsx index ca0bd9d8f6aa12..1507a1879cc19f 100644 --- a/packages/components/src/composite/hover.tsx +++ b/packages/components/src/composite/hover.tsx @@ -20,11 +20,11 @@ export const CompositeHover = forwardRef< WordPressComponentProps< CompositeHoverProps, 'div', false > >( function CompositeHover( props, ref ) { const context = useCompositeContext(); - return ( - - ); + + // @ts-expect-error The store prop is undocumented and only used by the + // legacy compat layer. The `store` prop is documented, but its type is + // obfuscated to discourage its use outside of the component's internals. + const store = ( props.store ?? context.store ) as Ariakit.CompositeStore; + + return ; } ); diff --git a/packages/components/src/composite/index.tsx b/packages/components/src/composite/index.tsx index e9e97072261fbf..8eb562f5bdab38 100644 --- a/packages/components/src/composite/index.tsx +++ b/packages/components/src/composite/index.tsx @@ -73,7 +73,10 @@ export const Composite = Object.assign( }, ref ) { - const store = Ariakit.useCompositeStore( { + // @ts-expect-error The store prop is undocumented and only used by the + // legacy compat layer. + const storeProp = props.store as Ariakit.CompositeStore; + const internalStore = Ariakit.useCompositeStore( { activeId, defaultActiveId, setActiveId, @@ -85,6 +88,8 @@ export const Composite = Object.assign( rtl, } ); + const store = storeProp ?? internalStore; + const contextValue = useMemo( () => ( { store, diff --git a/packages/components/src/composite/item.tsx b/packages/components/src/composite/item.tsx index 6d75b90f0baaaa..edbf0b92e039af 100644 --- a/packages/components/src/composite/item.tsx +++ b/packages/components/src/composite/item.tsx @@ -20,9 +20,27 @@ export const CompositeItem = forwardRef< WordPressComponentProps< CompositeItemProps, 'button', false > >( function CompositeItem( props, ref ) { const context = useCompositeContext(); + + // @ts-expect-error The store prop is undocumented and only used by the + // legacy compat layer. The `store` prop is documented, but its type is + // obfuscated to discourage its use outside of the component's internals. + const store = ( props.store ?? context.store ) as Ariakit.CompositeStore; + + // If the active item is not connected, Composite may end up in a state + // where none of the items are tabbable. In this case, we force all items to + // be tabbable, so that as soon as an item received focus, it becomes active + // and Composite goes back to working as expected. + const tabbable = Ariakit.useStoreState( store, ( state ) => { + return ( + state?.activeId !== null && + ! store?.item( state?.activeId )?.element?.isConnected + ); + } ); + return ( diff --git a/packages/components/src/composite/row.tsx b/packages/components/src/composite/row.tsx index a082af03ad6785..1a88da557785e9 100644 --- a/packages/components/src/composite/row.tsx +++ b/packages/components/src/composite/row.tsx @@ -20,11 +20,11 @@ export const CompositeRow = forwardRef< WordPressComponentProps< CompositeRowProps, 'div', false > >( function CompositeRow( props, ref ) { const context = useCompositeContext(); - return ( - - ); + + // @ts-expect-error The store prop is undocumented and only used by the + // legacy compat layer. The `store` prop is documented, but its type is + // obfuscated to discourage its use outside of the component's internals. + const store = ( props.store ?? context.store ) as Ariakit.CompositeStore; + + return ; } ); diff --git a/packages/components/src/composite/test/index.tsx b/packages/components/src/composite/test/index.tsx new file mode 100644 index 00000000000000..64619aaed01bd6 --- /dev/null +++ b/packages/components/src/composite/test/index.tsx @@ -0,0 +1,123 @@ +/** + * External dependencies + */ +import { queryByAttribute, render, screen } from '@testing-library/react'; +import { click, press, waitFor } from '@ariakit/test'; +import type { ComponentProps } from 'react'; + +/** + * WordPress dependencies + */ +import { useState } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { Composite } from '..'; + +// This is necessary because of how Ariakit calculates page up and +// page down. Without this, nothing has a height, and so paging up +// and down doesn't behave as expected in tests. + +let clientHeightSpy: jest.SpiedGetter< + typeof HTMLElement.prototype.clientHeight +>; + +beforeAll( () => { + clientHeightSpy = jest + .spyOn( HTMLElement.prototype, 'clientHeight', 'get' ) + .mockImplementation( function getClientHeight( this: HTMLElement ) { + if ( this.tagName === 'BODY' ) { + return window.outerHeight; + } + return 50; + } ); +} ); + +afterAll( () => { + clientHeightSpy?.mockRestore(); +} ); + +async function renderAndValidate( ...args: Parameters< typeof render > ) { + const view = render( ...args ); + await waitFor( () => { + const activeButton = queryByAttribute( + 'data-active-item', + view.baseElement, + 'true' + ); + expect( activeButton ).not.toBeNull(); + } ); + return view; +} + +function RemoveItemTest( props: ComponentProps< typeof Composite > ) { + const [ showThirdItem, setShowThirdItem ] = useState( true ); + return ( + <> + + + Item 1 + Item 2 + { showThirdItem && Item 3 } + + + + ); +} + +describe( 'Composite', () => { + it( 'should remain focusable even when there are no elements in the DOM associated with the currently active ID', async () => { + await renderAndValidate( ); + + const toggleButton = screen.getByRole( 'button', { + name: 'Toggle third item', + } ); + + await press.Tab(); + await press.Tab(); + + expect( + screen.getByRole( 'button', { name: 'Item 1' } ) + ).toHaveFocus(); + + await press.ArrowRight(); + await press.ArrowRight(); + + expect( + screen.getByRole( 'button', { name: 'Item 3' } ) + ).toHaveFocus(); + + await click( toggleButton ); + + expect( + screen.queryByRole( 'button', { name: 'Item 3' } ) + ).not.toBeInTheDocument(); + + await press.ShiftTab(); + + expect( + screen.getByRole( 'button', { name: 'Item 2' } ) + ).toHaveFocus(); + + await click( toggleButton ); + + expect( + screen.getByRole( 'button', { name: 'Item 3' } ) + ).toBeVisible(); + + await press.ShiftTab(); + + expect( + screen.getByRole( 'button', { name: 'Item 2' } ) + ).toHaveFocus(); + + await press.ArrowRight(); + + expect( + screen.getByRole( 'button', { name: 'Item 3' } ) + ).toHaveFocus(); + } ); +} ); diff --git a/packages/components/src/composite/typeahead.tsx b/packages/components/src/composite/typeahead.tsx index 771d58bcb6c25c..519c59ea374e5d 100644 --- a/packages/components/src/composite/typeahead.tsx +++ b/packages/components/src/composite/typeahead.tsx @@ -20,11 +20,11 @@ export const CompositeTypeahead = forwardRef< WordPressComponentProps< CompositeTypeaheadProps, 'div', false > >( function CompositeTypeahead( props, ref ) { const context = useCompositeContext(); - return ( - - ); + + // @ts-expect-error The store prop is undocumented and only used by the + // legacy compat layer. The `store` prop is documented, but its type is + // obfuscated to discourage its use outside of the component's internals. + const store = ( props.store ?? context.store ) as Ariakit.CompositeStore; + + return ; } ); diff --git a/packages/components/src/custom-gradient-picker/gradient-bar/control-points.tsx b/packages/components/src/custom-gradient-picker/gradient-bar/control-points.tsx index a92e973d88c5a8..3911e21e0f9348 100644 --- a/packages/components/src/custom-gradient-picker/gradient-bar/control-points.tsx +++ b/packages/components/src/custom-gradient-picker/gradient-bar/control-points.tsx @@ -56,7 +56,7 @@ function ControlPointButton( { <> diff --git a/packages/components/src/mobile/bottom-sheet-select-control/index.native.js b/packages/components/src/mobile/bottom-sheet-select-control/index.native.js index 89c157d85b9b2d..0e0a19c12aed44 100644 --- a/packages/components/src/mobile/bottom-sheet-select-control/index.native.js +++ b/packages/components/src/mobile/bottom-sheet-select-control/index.native.js @@ -64,7 +64,7 @@ const BottomSheetSelectControl = ( { onPress={ openSubSheet } accessibilityRole="button" accessibilityLabel={ sprintf( - // translators: %1$s: Select control button label e.g. "Button width". %2$s: Select control option value e.g: "Auto, 25%". + // translators: 1: Select control button label e.g. "Button width". 2: Select control option value e.g: "Auto, 25%". __( '%1$s. Currently selected: %2$s' ), label, selectedOption.label @@ -102,7 +102,7 @@ const BottomSheetSelectControl = ( { accessibilityLabel={ item.value === selectedValue ? sprintf( - // translators: %s: Select control option value e.g: "Auto, 25%". + // translators: %s: The selected option. __( 'Selected: %s' ), item.label ) diff --git a/packages/components/src/mobile/bottom-sheet/range-cell.native.js b/packages/components/src/mobile/bottom-sheet/range-cell.native.js index f0d82cb96908ac..afa25683e4e3e8 100644 --- a/packages/components/src/mobile/bottom-sheet/range-cell.native.js +++ b/packages/components/src/mobile/bottom-sheet/range-cell.native.js @@ -165,7 +165,7 @@ class BottomSheetRangeCell extends Component { const getAccessibilityLabel = () => { return sprintf( - /* translators: accessibility text. Inform about current value. %1$s: Control label %2$s: setting label (example: width), %3$s: Current value. %4$s: value measurement unit (example: pixels) */ + /* translators: accessibility text. Inform about current value. 1: Control label. 2: setting label (example: width). 3: Current value. 4: value measurement unit (example: pixels) */ __( '%1$s. %2$s is %3$s %4$s.' ), cellProps.label, settingLabel, diff --git a/packages/components/src/mobile/bottom-sheet/stepper-cell/index.native.js b/packages/components/src/mobile/bottom-sheet/stepper-cell/index.native.js index aad78cd12665b6..6e5239687c5aa4 100644 --- a/packages/components/src/mobile/bottom-sheet/stepper-cell/index.native.js +++ b/packages/components/src/mobile/bottom-sheet/stepper-cell/index.native.js @@ -159,7 +159,7 @@ class BottomSheetStepperCell extends Component { }; const accessibilityLabel = sprintf( - /* translators: accessibility text. Inform about current value. %1$s: Control label %2$s: setting label (example: width), %3$s: Current value. %4$s: value measurement unit (example: pixels) */ + /* translators: accessibility text. Inform about current value. 1: Control label. 2: setting label (example: width). 3: Current value. 4: value measurement unit (example: pixels) */ __( '%1$s. %2$s is %3$s %4$s.' ), label, settingLabel, diff --git a/packages/components/src/navigator/navigator-provider/component.tsx b/packages/components/src/navigator/navigator-provider/component.tsx index ebcb247c574830..01254b743f87d0 100644 --- a/packages/components/src/navigator/navigator-provider/component.tsx +++ b/packages/components/src/navigator/navigator-provider/component.tsx @@ -66,7 +66,7 @@ function goTo( options: NavigateOptions = {} ) { const { focusSelectors } = state; - const currentLocation = { ...state.currentLocation, isInitial: false }; + const currentLocation = { ...state.currentLocation }; const { // Default assignments @@ -114,6 +114,7 @@ function goTo( return { currentLocation: { ...restOptions, + isInitial: false, path, isBack, hasRestoredFocus: false, @@ -129,7 +130,7 @@ function goToParent( options: NavigateToParentOptions = {} ) { const { screens, focusSelectors } = state; - const currentLocation = { ...state.currentLocation, isInitial: false }; + const currentLocation = { ...state.currentLocation }; const currentPath = currentLocation.path; if ( currentPath === undefined ) { return { currentLocation, focusSelectors }; diff --git a/packages/components/src/palette-edit/index.tsx b/packages/components/src/palette-edit/index.tsx index 601ea758b75bba..8bfcb7240b9ea4 100644 --- a/packages/components/src/palette-edit/index.tsx +++ b/packages/components/src/palette-edit/index.tsx @@ -49,7 +49,6 @@ import { kebabCase } from '../utils/strings'; import type { Color, ColorPickerPopoverProps, - Gradient, NameInputProps, OptionProps, PaletteEditListViewProps, @@ -70,6 +69,28 @@ function NameInput( { value, onChange, label }: NameInputProps ) { ); } +/* + * Deduplicates the slugs of the provided elements. + */ +export function deduplicateElementSlugs< T extends PaletteElement >( + elements: T[] +) { + const slugCounts: { [ slug: string ]: number } = {}; + + return elements.map( ( element ) => { + let newSlug: string | undefined; + + const { slug } = element; + slugCounts[ slug ] = ( slugCounts[ slug ] || 0 ) + 1; + + if ( slugCounts[ slug ] > 1 ) { + newSlug = `${ slug }-${ slugCounts[ slug ] - 1 }`; + } + + return { ...element, slug: newSlug ?? slug }; + } ); +} + /** * Returns a name and slug for a palette item. The name takes the format "Color + id". * To ensure there are no duplicate ids, this function checks all slugs. @@ -109,7 +130,7 @@ export function getNameAndSlugForPosition( }; } -function ColorPickerPopover< T extends Color | Gradient >( { +function ColorPickerPopover< T extends PaletteElement >( { isGradient, element, onChange, @@ -167,7 +188,7 @@ function ColorPickerPopover< T extends Color | Gradient >( { ); } -function Option< T extends Color | Gradient >( { +function Option< T extends PaletteElement >( { canOnlyChangeValues, element, onChange, @@ -265,7 +286,7 @@ function Option< T extends Color | Gradient >( { ); } -function PaletteEditListView< T extends Color | Gradient >( { +function PaletteEditListView< T extends PaletteElement >( { elements, onChange, canOnlyChangeValues, @@ -280,7 +301,11 @@ function PaletteEditListView< T extends Color | Gradient >( { elementsReferenceRef.current = elements; }, [ elements ] ); - const debounceOnChange = useDebounce( onChange, 100 ); + const debounceOnChange = useDebounce( + ( updatedElements: T[] ) => + onChange( deduplicateElementSlugs( updatedElements ) ), + 100 + ); return ( diff --git a/packages/components/src/palette-edit/test/index.tsx b/packages/components/src/palette-edit/test/index.tsx index 980630633b97f9..7dc00dbba22042 100644 --- a/packages/components/src/palette-edit/test/index.tsx +++ b/packages/components/src/palette-edit/test/index.tsx @@ -7,7 +7,10 @@ import { click, type, press } from '@ariakit/test'; /** * Internal dependencies */ -import PaletteEdit, { getNameAndSlugForPosition } from '..'; +import PaletteEdit, { + getNameAndSlugForPosition, + deduplicateElementSlugs, +} from '..'; import type { PaletteElement } from '../types'; const noop = () => {}; @@ -97,6 +100,52 @@ describe( 'getNameAndSlugForPosition', () => { } ); } ); +describe( 'deduplicateElementSlugs', () => { + it( 'should not change the slugs if they are unique', () => { + const elements: PaletteElement[] = [ + { + slug: 'test-color-1', + color: '#ffffff', + name: 'Test Color 1', + }, + { + slug: 'test-color-2', + color: '#1a4548', + name: 'Test Color 2', + }, + ]; + + expect( deduplicateElementSlugs( elements ) ).toEqual( elements ); + } ); + it( 'should change the slugs if they are not unique', () => { + const elements: PaletteElement[] = [ + { + slug: 'test-color-1', + color: '#ffffff', + name: 'Test Color 1', + }, + { + slug: 'test-color-1', + color: '#1a4548', + name: 'Test Color 2', + }, + ]; + + expect( deduplicateElementSlugs( elements ) ).toEqual( [ + { + slug: 'test-color-1', + color: '#ffffff', + name: 'Test Color 1', + }, + { + slug: 'test-color-1-1', + color: '#1a4548', + name: 'Test Color 2', + }, + ] ); + } ); +} ); + describe( 'PaletteEdit', () => { const defaultProps = { paletteLabel: 'Test label', diff --git a/packages/components/src/tabs/styles.ts b/packages/components/src/tabs/styles.ts index c00943b180f637..07f3cb9600bb2e 100644 --- a/packages/components/src/tabs/styles.ts +++ b/packages/components/src/tabs/styles.ts @@ -100,7 +100,7 @@ export const Tab = styled( Ariakit.Tab )` box-shadow: none; cursor: pointer; line-height: 1.2; // Some languages characters e.g. Japanese may have a native higher line-height. - padding: ${ space( 4 ) }; + padding: ${ space( 3 ) } ${ space( 4 ) }; margin-left: 0; font-weight: 500; text-align: inherit; diff --git a/packages/components/src/toggle-group-control/test/__snapshots__/index.tsx.snap b/packages/components/src/toggle-group-control/test/__snapshots__/index.tsx.snap index e9b4f4ca22ab85..34b7fa9575a546 100644 --- a/packages/components/src/toggle-group-control/test/__snapshots__/index.tsx.snap +++ b/packages/components/src/toggle-group-control/test/__snapshots__/index.tsx.snap @@ -244,7 +244,7 @@ exports[`ToggleGroupControl controlled should render correctly with icons 1`] = class="components-toggle-group-control emotion-8 emotion-9" data-wp-c16t="true" data-wp-component="ToggleGroupControl" - id="toggle-group-control-as-radio-group-11" + id="toggle-group-control-as-radio-group-12" role="radiogroup" > diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-class/view.js b/packages/e2e-tests/plugins/interactive-blocks/directive-class/view.js index f008ae910673fd..9ae5207c0e05c5 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/directive-class/view.js +++ b/packages/e2e-tests/plugins/interactive-blocks/directive-class/view.js @@ -15,9 +15,9 @@ const { state } = store( 'directive-class', { toggleFalseValue: () => { state.falseValue = ! state.falseValue; }, - toggleContextFalseValue: () => { + toggleContextValue: () => { const context = getContext(); - context.falseValue = ! context.falseValue; + context.value = ! context.value; }, }, } ); diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-init/view.js b/packages/e2e-tests/plugins/interactive-blocks/directive-init/view.js index 9990e2743c8799..a8c70a4a907207 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/directive-init/view.js +++ b/packages/e2e-tests/plugins/interactive-blocks/directive-init/view.js @@ -12,7 +12,7 @@ const { directive } = privateApis( directive( 'show-mock', ( { directives: { 'show-mock': showMock }, element, evaluate } ) => { - const entry = showMock.find( ( { suffix } ) => suffix === 'default' ); + const entry = showMock.find( ( { suffix } ) => suffix === null ); if ( ! evaluate( entry ) ) { return null; } diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-on-document/view.js b/packages/e2e-tests/plugins/interactive-blocks/directive-on-document/view.js index 19ffc2a530193b..b9689ac978f85f 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/directive-on-document/view.js +++ b/packages/e2e-tests/plugins/interactive-blocks/directive-on-document/view.js @@ -12,7 +12,7 @@ const { directive } = privateApis( directive( 'show-mock', ( { directives: { 'show-mock': showMock }, element, evaluate } ) => { - const entry = showMock.find( ( { suffix } ) => suffix === 'default' ); + const entry = showMock.find( ( { suffix } ) => suffix === null ); if ( ! evaluate( entry ) ) { return null; } diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-on-window/view.js b/packages/e2e-tests/plugins/interactive-blocks/directive-on-window/view.js index e22379ad4d0775..ef72e266e10759 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/directive-on-window/view.js +++ b/packages/e2e-tests/plugins/interactive-blocks/directive-on-window/view.js @@ -12,7 +12,7 @@ const { directive } = privateApis( directive( 'show-mock', ( { directives: { 'show-mock': showMock }, element, evaluate } ) => { - const entry = showMock.find( ( { suffix } ) => suffix === 'default' ); + const entry = showMock.find( ( { suffix } ) => suffix === null ); if ( ! evaluate( entry ) ) { return null; } diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-priorities/view.js b/packages/e2e-tests/plugins/interactive-blocks/directive-priorities/view.js index 5a46908f77d87b..77f2f25c5f9a41 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/directive-priorities/view.js +++ b/packages/e2e-tests/plugins/interactive-blocks/directive-priorities/view.js @@ -41,13 +41,13 @@ directive( 'test-context', ( { context: { Provider }, props: { children } } ) => { executionProof( 'context' ); - const value = { + const client = { [ namespace ]: proxifyState( namespace, { attribute: 'from context', text: 'from context', } ), }; - return h( Provider, { value }, children ); + return h( Provider, { value: { client } }, children ); }, { priority: 8 } ); diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-run/view.js b/packages/e2e-tests/plugins/interactive-blocks/directive-run/view.js index cb9be34b2460a8..125ac392042306 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/directive-run/view.js +++ b/packages/e2e-tests/plugins/interactive-blocks/directive-run/view.js @@ -21,9 +21,7 @@ directive( element, evaluate, } ) => { - const entry = showChildren.find( - ( { suffix } ) => suffix === 'default' - ); + const entry = showChildren.find( ( { suffix } ) => suffix === null ); return evaluate( entry ) ? element : cloneElement( element, { children: null } ); diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-watch/view.js b/packages/e2e-tests/plugins/interactive-blocks/directive-watch/view.js index af2d452a104319..ad035811a0bcd7 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/directive-watch/view.js +++ b/packages/e2e-tests/plugins/interactive-blocks/directive-watch/view.js @@ -12,7 +12,7 @@ const { directive } = privateApis( directive( 'show-mock', ( { directives: { 'show-mock': showMock }, element, evaluate } ) => { - const entry = showMock.find( ( { suffix } ) => suffix === 'default' ); + const entry = showMock.find( ( { suffix } ) => suffix === null ); if ( ! evaluate( entry ) ) { return null; } diff --git a/packages/e2e-tests/plugins/interactive-blocks/get-server-context/block.json b/packages/e2e-tests/plugins/interactive-blocks/get-server-context/block.json new file mode 100644 index 00000000000000..c635846328b9e4 --- /dev/null +++ b/packages/e2e-tests/plugins/interactive-blocks/get-server-context/block.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 2, + "name": "test/get-server-context", + "title": "E2E Interactivity tests - getServerContext", + "category": "text", + "icon": "heart", + "description": "", + "supports": { + "interactivity": true + }, + "textdomain": "e2e-interactivity", + "viewScriptModule": "file:./view.js", + "render": "file:./render.php" +} diff --git a/packages/e2e-tests/plugins/interactive-blocks/get-server-context/render.php b/packages/e2e-tests/plugins/interactive-blocks/get-server-context/render.php new file mode 100644 index 00000000000000..a71ced20dc46a1 --- /dev/null +++ b/packages/e2e-tests/plugins/interactive-blocks/get-server-context/render.php @@ -0,0 +1,51 @@ + + + + +
+> +
+ > +
+
+
+
+
+
+ + +
+
diff --git a/packages/e2e-tests/plugins/interactive-blocks/get-server-context/view.asset.php b/packages/e2e-tests/plugins/interactive-blocks/get-server-context/view.asset.php new file mode 100644 index 00000000000000..bdaec8d1b67a9d --- /dev/null +++ b/packages/e2e-tests/plugins/interactive-blocks/get-server-context/view.asset.php @@ -0,0 +1,9 @@ + array( + '@wordpress/interactivity', + array( + 'id' => '@wordpress/interactivity-router', + 'import' => 'dynamic', + ), + ), +); diff --git a/packages/e2e-tests/plugins/interactive-blocks/get-server-context/view.js b/packages/e2e-tests/plugins/interactive-blocks/get-server-context/view.js new file mode 100644 index 00000000000000..83f016e2eac16a --- /dev/null +++ b/packages/e2e-tests/plugins/interactive-blocks/get-server-context/view.js @@ -0,0 +1,46 @@ +/** + * WordPress dependencies + */ +import { store, getContext, getServerContext } from '@wordpress/interactivity'; + +store( 'test/get-server-context', { + actions: { + *navigate( e ) { + e.preventDefault(); + const { actions } = yield import( + '@wordpress/interactivity-router' + ); + yield actions.navigate( e.target.href ); + }, + attemptModification() { + try { + getServerContext().prop = 'updated from client'; + getContext().result = 'unexpectedly modified ❌'; + } catch ( e ) { + getContext().result = 'not modified ✅'; + } + }, + }, + callbacks: { + updateServerContextParent() { + const ctx = getContext(); + const { prop, newProp, nested, inherited } = getServerContext(); + ctx.prop = prop; + ctx.newProp = newProp; + ctx.nested.prop = nested.prop; + ctx.nested.newProp = nested.newProp; + ctx.inherited.prop = inherited.prop; + ctx.inherited.newProp = inherited.newProp; + }, + updateServerContextChild() { + const ctx = getContext(); + const { prop, newProp, nested, inherited } = getServerContext(); + ctx.prop = prop; + ctx.newProp = newProp; + ctx.nested.prop = nested.prop; + ctx.nested.newProp = nested.newProp; + ctx.inherited.prop = inherited.prop; + ctx.inherited.newProp = inherited.newProp; + }, + }, +} ); diff --git a/packages/e2e-tests/plugins/interactive-blocks/get-server-state/block.json b/packages/e2e-tests/plugins/interactive-blocks/get-server-state/block.json new file mode 100644 index 00000000000000..abf76eb9beddcc --- /dev/null +++ b/packages/e2e-tests/plugins/interactive-blocks/get-server-state/block.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 2, + "name": "test/get-server-state", + "title": "E2E Interactivity tests - getServerState", + "category": "text", + "icon": "heart", + "description": "", + "supports": { + "interactivity": true + }, + "textdomain": "e2e-interactivity", + "viewScriptModule": "file:./view.js", + "render": "file:./render.php" +} diff --git a/packages/e2e-tests/plugins/interactive-blocks/get-server-state/render.php b/packages/e2e-tests/plugins/interactive-blocks/get-server-state/render.php new file mode 100644 index 00000000000000..abc4efd8272d5b --- /dev/null +++ b/packages/e2e-tests/plugins/interactive-blocks/get-server-state/render.php @@ -0,0 +1,50 @@ + + +
+
+
+
+
+ + + + + +
diff --git a/packages/e2e-tests/plugins/interactive-blocks/get-server-state/view.asset.php b/packages/e2e-tests/plugins/interactive-blocks/get-server-state/view.asset.php new file mode 100644 index 00000000000000..bdaec8d1b67a9d --- /dev/null +++ b/packages/e2e-tests/plugins/interactive-blocks/get-server-state/view.asset.php @@ -0,0 +1,9 @@ + array( + '@wordpress/interactivity', + array( + 'id' => '@wordpress/interactivity-router', + 'import' => 'dynamic', + ), + ), +); diff --git a/packages/e2e-tests/plugins/interactive-blocks/get-server-state/view.js b/packages/e2e-tests/plugins/interactive-blocks/get-server-state/view.js new file mode 100644 index 00000000000000..db2992ec4a5863 --- /dev/null +++ b/packages/e2e-tests/plugins/interactive-blocks/get-server-state/view.js @@ -0,0 +1,33 @@ +/** + * WordPress dependencies + */ +import { store, getServerState, getContext } from '@wordpress/interactivity'; + +const { state } = store( 'test/get-server-state', { + actions: { + *navigate( e ) { + e.preventDefault(); + const { actions } = yield import( + '@wordpress/interactivity-router' + ); + yield actions.navigate( e.target.href ); + }, + attemptModification() { + try { + getServerState().prop = 'updated from client'; + getContext().result = 'unexpectedly modified ❌'; + } catch ( e ) { + getContext().result = 'not modified ✅'; + } + }, + }, + callbacks: { + updateState() { + const { prop, newProp, nested } = getServerState(); + state.prop = prop; + state.newProp = newProp; + state.nested.prop = nested.prop; + state.nested.newProp = nested.newProp; + }, + }, +} ); diff --git a/packages/e2e-tests/plugins/interactive-blocks/tovdom-islands/view.js b/packages/e2e-tests/plugins/interactive-blocks/tovdom-islands/view.js index 2993a273486c2b..8016e931624a16 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/tovdom-islands/view.js +++ b/packages/e2e-tests/plugins/interactive-blocks/tovdom-islands/view.js @@ -12,7 +12,7 @@ const { directive, h } = privateApis( directive( 'show-mock', ( { directives: { 'show-mock': showMock }, element, evaluate } ) => { - const entry = showMock.find( ( { suffix } ) => suffix === 'default' ); + const entry = showMock.find( ( { suffix } ) => suffix === null ); if ( ! evaluate( entry ) ) { element.props.children = h( diff --git a/packages/edit-post/CHANGELOG.md b/packages/edit-post/CHANGELOG.md index cd4b4a36295f07..3eb52f26e65012 100644 --- a/packages/edit-post/CHANGELOG.md +++ b/packages/edit-post/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 8.8.0 (2024-09-19) + ## 8.7.0 (2024-09-05) ## 8.6.0 (2024-08-21) diff --git a/packages/edit-post/package.json b/packages/edit-post/package.json index 0d8e0b28f7c7c5..fea3d8ef832469 100644 --- a/packages/edit-post/package.json +++ b/packages/edit-post/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/edit-post", - "version": "8.7.0", + "version": "8.8.17", "description": "Edit Post module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/edit-post/src/components/layout/index.js b/packages/edit-post/src/components/layout/index.js index e6451c6a4a4082..2f6127f89b9702 100644 --- a/packages/edit-post/src/components/layout/index.js +++ b/packages/edit-post/src/components/layout/index.js @@ -31,6 +31,7 @@ import { useRef, useState, } from '@wordpress/element'; +import { chevronDown, chevronUp } from '@wordpress/icons'; import { store as noticesStore } from '@wordpress/notices'; import { store as preferencesStore } from '@wordpress/preferences'; import { @@ -43,10 +44,12 @@ import { addQueryArgs } from '@wordpress/url'; import { decodeEntities } from '@wordpress/html-entities'; import { store as coreStore } from '@wordpress/core-data'; import { + Icon, ResizableBox, SlotFillProvider, Tooltip, VisuallyHidden, + __unstableUseNavigateRegions as useNavigateRegions, } from '@wordpress/components'; import { useMediaQuery, @@ -75,7 +78,7 @@ import useNavigateToEntityRecord from '../../hooks/use-navigate-to-entity-record const { getLayoutStyles } = unlock( blockEditorPrivateApis ); const { useCommands } = unlock( coreCommandsPrivateApis ); const { useCommandContext } = unlock( commandsPrivateApis ); -const { Editor, FullscreenMode } = unlock( editorPrivateApis ); +const { Editor, FullscreenMode, NavigableRegion } = unlock( editorPrivateApis ); const { BlockKeyboardShortcuts } = unlock( blockLibraryPrivateApis ); const DESIGN_POST_TYPES = [ 'wp_template', @@ -183,7 +186,7 @@ function MetaBoxesMain( { isLegacy } ) { ]; }, [] ); const { set: setPreference } = useDispatch( preferencesStore ); - const resizableBoxRef = useRef(); + const metaBoxesMainRef = useRef(); const isShort = useMediaQuery( '(max-height: 549px)' ); const [ { min, max }, setHeightConstraints ] = useState( () => ( {} ) ); @@ -198,9 +201,9 @@ function MetaBoxesMain( { isLegacy } ) { ':scope > .components-notice-list' ); const resizeHandle = container.querySelector( - '.edit-post-meta-boxes-main__resize-handle' + '.edit-post-meta-boxes-main__presenter' ); - const actualize = () => { + const deriveConstraints = () => { const fullHeight = container.offsetHeight; let nextMax = fullHeight; for ( const element of noticeLists ) { @@ -209,7 +212,7 @@ function MetaBoxesMain( { isLegacy } ) { const nextMin = resizeHandle.offsetHeight; setHeightConstraints( { min: nextMin, max: nextMax } ); }; - const observer = new window.ResizeObserver( actualize ); + const observer = new window.ResizeObserver( deriveConstraints ); observer.observe( container ); for ( const element of noticeLists ) { observer.observe( element ); @@ -221,12 +224,33 @@ function MetaBoxesMain( { isLegacy } ) { const separatorHelpId = useId(); const [ isUntouched, setIsUntouched ] = useState( true ); + const applyHeight = ( candidateHeight, isPersistent, isInstant ) => { + const nextHeight = Math.min( max, Math.max( min, candidateHeight ) ); + if ( isPersistent ) { + setPreference( + 'core/edit-post', + 'metaBoxesMainOpenHeight', + nextHeight + ); + } else { + separatorRef.current.ariaValueNow = getAriaValueNow( nextHeight ); + } + if ( isInstant ) { + metaBoxesMainRef.current.updateSize( { + height: nextHeight, + // Oddly, when the event that triggered this was not from the mouse (e.g. keydown), + // if `width` is left unspecified a subsequent drag gesture applies a fixed + // width and the pane fails to widen/narrow with parent width changes from + // sidebars opening/closing or window resizes. + width: 'auto', + } ); + } + }; if ( ! hasAnyVisible ) { return; } - const className = 'edit-post-meta-boxes-main'; const contents = ( ) } - shortcuts={ { - previous: previousShortcut, - next: nextShortcut, - } } /> ); } diff --git a/packages/editor/CHANGELOG.md b/packages/editor/CHANGELOG.md index b380c2fd296d55..6f31c1735021f6 100644 --- a/packages/editor/CHANGELOG.md +++ b/packages/editor/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 14.8.0 (2024-09-19) + ## 14.7.0 (2024-09-05) ## 14.6.0 (2024-08-21) diff --git a/packages/editor/package.json b/packages/editor/package.json index 2e01aff3a0aa5f..18d698aa8c7687 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/editor", - "version": "14.7.0", + "version": "14.8.17", "description": "Enhanced block editor for WordPress posts.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/editor/src/bindings/api.js b/packages/editor/src/bindings/api.js index 2cfed5168a143e..2d32d76abbc3bc 100644 --- a/packages/editor/src/bindings/api.js +++ b/packages/editor/src/bindings/api.js @@ -1,18 +1,13 @@ /** * WordPress dependencies */ -import { - privateApis as blocksPrivateApis, - store as blocksStore, -} from '@wordpress/blocks'; -import { dispatch } from '@wordpress/data'; +import { registerBlockBindingsSource } from '@wordpress/blocks'; /** * Internal dependencies */ import patternOverrides from './pattern-overrides'; import postMeta from './post-meta'; -import { unlock } from '../lock-unlock'; /** * Function to register core block bindings sources provided by the editor. @@ -25,33 +20,6 @@ import { unlock } from '../lock-unlock'; * ``` */ export function registerCoreBlockBindingsSources() { - const { registerBlockBindingsSource } = unlock( blocksPrivateApis ); registerBlockBindingsSource( patternOverrides ); registerBlockBindingsSource( postMeta ); } - -/** - * Function to bootstrap core block bindings sources defined in the server. - * - * @param {Object} sources Object containing the sources to bootstrap. - * - * @example - * ```js - * import { bootstrapBlockBindingsSourcesFromServer } from '@wordpress/editor'; - * - * bootstrapBlockBindingsSourcesFromServer( sources ); - * ``` - */ -export function bootstrapBlockBindingsSourcesFromServer( sources ) { - if ( sources ) { - const { addBootstrappedBlockBindingsSource } = unlock( - dispatch( blocksStore ) - ); - for ( const [ name, args ] of Object.entries( sources ) ) { - addBootstrappedBlockBindingsSource( { - name, - ...args, - } ); - } - } -} diff --git a/packages/editor/src/bindings/pattern-overrides.js b/packages/editor/src/bindings/pattern-overrides.js index 88c6c73bdc61c1..baa1f72f47694b 100644 --- a/packages/editor/src/bindings/pattern-overrides.js +++ b/packages/editor/src/bindings/pattern-overrides.js @@ -7,9 +7,9 @@ const CONTENT = 'content'; export default { name: 'core/pattern-overrides', - getValues( { registry, clientId, context, bindings } ) { + getValues( { select, clientId, context, bindings } ) { const patternOverridesContent = context[ 'pattern/overrides' ]; - const { getBlockAttributes } = registry.select( blockEditorStore ); + const { getBlockAttributes } = select( blockEditorStore ); const currentBlockAttributes = getBlockAttributes( clientId ); const overridesValues = {}; @@ -32,9 +32,9 @@ export default { } return overridesValues; }, - setValues( { registry, clientId, bindings } ) { + setValues( { select, dispatch, clientId, bindings } ) { const { getBlockAttributes, getBlockParentsByBlockName, getBlocks } = - registry.select( blockEditorStore ); + select( blockEditorStore ); const currentBlockAttributes = getBlockAttributes( clientId ); const blockName = currentBlockAttributes?.metadata?.name; if ( ! blockName ) { @@ -61,12 +61,10 @@ export default { const syncBlocksWithSameName = ( blocks ) => { for ( const block of blocks ) { if ( block.attributes?.metadata?.name === blockName ) { - registry - .dispatch( blockEditorStore ) - .updateBlockAttributes( - block.clientId, - attributes - ); + dispatch( blockEditorStore ).updateBlockAttributes( + block.clientId, + attributes + ); } syncBlocksWithSameName( block.innerBlocks ); } @@ -77,27 +75,26 @@ export default { } const currentBindingValue = getBlockAttributes( patternClientId )?.[ CONTENT ]; - registry - .dispatch( blockEditorStore ) - .updateBlockAttributes( patternClientId, { - [ CONTENT ]: { - ...currentBindingValue, - [ blockName ]: { - ...currentBindingValue?.[ blockName ], - ...Object.entries( attributes ).reduce( - ( acc, [ key, value ] ) => { - // TODO: We need a way to represent `undefined` in the serialized overrides. - // Also see: https://github.com/WordPress/gutenberg/pull/57249#discussion_r1452987871 - // We use an empty string to represent undefined for now until - // we support a richer format for overrides and the block bindings API. - acc[ key ] = value === undefined ? '' : value; - return acc; - }, - {} - ), - }, + + dispatch( blockEditorStore ).updateBlockAttributes( patternClientId, { + [ CONTENT ]: { + ...currentBindingValue, + [ blockName ]: { + ...currentBindingValue?.[ blockName ], + ...Object.entries( attributes ).reduce( + ( acc, [ key, value ] ) => { + // TODO: We need a way to represent `undefined` in the serialized overrides. + // Also see: https://github.com/WordPress/gutenberg/pull/57249#discussion_r1452987871 + // We use an empty string to represent undefined for now until + // we support a richer format for overrides and the block bindings API. + acc[ key ] = value === undefined ? '' : value; + return acc; + }, + {} + ), }, - } ); + }, + } ); }, canUserEditValue: () => true, }; diff --git a/packages/editor/src/bindings/post-meta.js b/packages/editor/src/bindings/post-meta.js index 572cd0b525a003..a3602ce7d62076 100644 --- a/packages/editor/src/bindings/post-meta.js +++ b/packages/editor/src/bindings/post-meta.js @@ -9,26 +9,64 @@ import { store as coreDataStore } from '@wordpress/core-data'; import { store as editorStore } from '../store'; import { unlock } from '../lock-unlock'; -function getMetadata( registry, context, registeredFields ) { - let metaFields = {}; - const type = registry.select( editorStore ).getCurrentPostType(); - const { getEditedEntityRecord } = registry.select( coreDataStore ); +/** + * Gets a list of post meta fields with their values and labels + * to be consumed in the needed callbacks. + * If the value is not available based on context, like in templates, + * it falls back to the default value, label, or key. + * + * @param {Object} select The select function from the data store. + * @param {Object} context The context provided. + * @return {Object} List of post meta fields with their value and label. + * + * @example + * ```js + * { + * field_1_key: { + * label: 'Field 1 Label', + * value: 'Field 1 Value', + * }, + * field_2_key: { + * label: 'Field 2 Label', + * value: 'Field 2 Value', + * }, + * ... + * } + * ``` + */ +function getPostMetaFields( select, context ) { + const { getEditedEntityRecord } = select( coreDataStore ); + const { getRegisteredPostMeta } = unlock( select( coreDataStore ) ); + let entityMetaValues; + // Try to get the current entity meta values. if ( context?.postType && context?.postId ) { - metaFields = getEditedEntityRecord( + entityMetaValues = getEditedEntityRecord( 'postType', context?.postType, context?.postId ).meta; - } else if ( type === 'wp_template' ) { - // Populate the `metaFields` object with the default values. - Object.entries( registeredFields || {} ).forEach( - ( [ key, props ] ) => { - if ( props.default ) { - metaFields[ key ] = props.default; - } - } - ); + } + + const registeredFields = getRegisteredPostMeta( context?.postType ); + const metaFields = {}; + Object.entries( registeredFields || {} ).forEach( ( [ key, props ] ) => { + // Don't include footnotes or private fields. + if ( key !== 'footnotes' && key.charAt( 0 ) !== '_' ) { + metaFields[ key ] = { + label: props.title || key, + value: + // When using the entity value, an empty string IS a valid value. + entityMetaValues?.[ key ] ?? + // When using the default, an empty string IS NOT a valid value. + ( props.default || undefined ), + type: props.type, + }; + } + } ); + + if ( ! Object.keys( metaFields || {} ).length ) { + return null; } return metaFields; @@ -36,34 +74,33 @@ function getMetadata( registry, context, registeredFields ) { export default { name: 'core/post-meta', - getValues( { registry, context, bindings } ) { - const { getRegisteredPostMeta } = unlock( - registry.select( coreDataStore ) - ); - const registeredFields = getRegisteredPostMeta( context?.postType ); - const metaFields = getMetadata( registry, context, registeredFields ); + getValues( { select, context, bindings } ) { + const metaFields = getPostMetaFields( select, context ); const newValues = {}; for ( const [ attributeName, source ] of Object.entries( bindings ) ) { // Use the value, the field label, or the field key. - const metaKey = source.args.key; - newValues[ attributeName ] = - metaFields?.[ metaKey ] ?? - registeredFields?.[ metaKey ]?.title ?? - metaKey; + const fieldKey = source.args.key; + const { value: fieldValue, label: fieldLabel } = + metaFields?.[ fieldKey ] || {}; + newValues[ attributeName ] = fieldValue ?? fieldLabel ?? fieldKey; } return newValues; }, - setValues( { registry, context, bindings } ) { + setValues( { dispatch, context, bindings } ) { const newMeta = {}; Object.values( bindings ).forEach( ( { args, newValue } ) => { newMeta[ args.key ] = newValue; } ); - registry - .dispatch( coreDataStore ) - .editEntityRecord( 'postType', context?.postType, context?.postId, { + + dispatch( coreDataStore ).editEntityRecord( + 'postType', + context?.postType, + context?.postId, + { meta: newMeta, - } ); + } + ); }, canUserEditValue( { select, context, args } ) { // Lock editing in query loop. @@ -79,14 +116,9 @@ export default { return false; } - // Check that the custom field is not protected and available in the REST API. + const fieldValue = getPostMetaFields( select, context )?.[ args.key ] + ?.value; // Empty string or `false` could be a valid value, so we need to check if the field value is undefined. - const fieldValue = select( coreDataStore ).getEntityRecord( - 'postType', - postType, - context?.postId - )?.meta?.[ args.key ]; - if ( fieldValue === undefined ) { return false; } @@ -109,32 +141,7 @@ export default { return true; }, - getFieldsList( { registry, context } ) { - const { getRegisteredPostMeta } = unlock( - registry.select( coreDataStore ) - ); - const registeredFields = getRegisteredPostMeta( context?.postType ); - const metaFields = getMetadata( registry, context, registeredFields ); - - if ( ! metaFields || ! Object.keys( metaFields ).length ) { - return null; - } - - return Object.fromEntries( - Object.entries( metaFields ) - // Remove footnotes or private keys from the list of fields. - .filter( - ( [ key ] ) => - key !== 'footnotes' && key.charAt( 0 ) !== '_' - ) - // Return object with label and value. - .map( ( [ key, value ] ) => [ - key, - { - label: registeredFields?.[ key ]?.title || key, - value, - }, - ] ) - ); + getFieldsList( { select, context } ) { + return getPostMetaFields( select, context ); }, }; diff --git a/packages/editor/src/components/block-settings-menu/content-only-settings-menu.js b/packages/editor/src/components/block-settings-menu/content-only-settings-menu.js index fcf7adfa77635c..e4b6089bf2d92e 100644 --- a/packages/editor/src/components/block-settings-menu/content-only-settings-menu.js +++ b/packages/editor/src/components/block-settings-menu/content-only-settings-menu.js @@ -46,12 +46,17 @@ function ContentOnlySettingsMenuItems( { clientId, onClose } ) { getBlockAttributes( patternParent ).ref ); } else { - const { getCurrentTemplateId } = select( editorStore ); + const { getCurrentTemplateId, getRenderingMode } = + select( editorStore ); const templateId = getCurrentTemplateId(); const { getContentLockingParent } = unlock( select( blockEditorStore ) ); - if ( ! getContentLockingParent( clientId ) && templateId ) { + if ( + getRenderingMode() === 'template-locked' && + ! getContentLockingParent( clientId ) && + templateId + ) { record = select( coreStore ).getEntityRecord( 'postType', 'wp_template', diff --git a/packages/editor/src/components/editor-interface/index.js b/packages/editor/src/components/editor-interface/index.js index 645e5fb6f53a26..9f7f3fb7ad4b0e 100644 --- a/packages/editor/src/components/editor-interface/index.js +++ b/packages/editor/src/components/editor-interface/index.js @@ -15,7 +15,6 @@ import { BlockBreadcrumb, BlockToolbar, } from '@wordpress/block-editor'; -import { store as keyboardShortcutsStore } from '@wordpress/keyboard-shortcuts'; import { useViewportMatch } from '@wordpress/compose'; import { useState, useCallback } from '@wordpress/element'; @@ -32,6 +31,8 @@ import TextEditor from '../text-editor'; import VisualEditor from '../visual-editor'; import EditorContentSlotFill from './content-slot-fill'; +import { unlock } from '../../lock-unlock'; + const interfaceLabels = { /* translators: accessibility text for the editor top bar landmark region. */ header: __( 'Editor top bar' ), @@ -47,7 +48,6 @@ const interfaceLabels = { export default function EditorInterface( { className, - enableRegionNavigation, styles, children, forceIsDirty, @@ -67,16 +67,15 @@ export default function EditorInterface( { isListViewOpened, isDistractionFree, isPreviewMode, - previousShortcut, - nextShortcut, showBlockBreadcrumbs, documentLabel, - blockEditorMode, + isZoomOut, } = useSelect( ( select ) => { const { get } = select( preferencesStore ); const { getEditorSettings, getPostTypeLabel } = select( editorStore ); const editorSettings = getEditorSettings(); const postTypeLabel = getPostTypeLabel(); + const { isZoomOut: _isZoomOut } = unlock( select( blockEditorStore ) ); return { mode: select( editorStore ).getEditorMode(), @@ -85,17 +84,11 @@ export default function EditorInterface( { isListViewOpened: select( editorStore ).isListViewOpened(), isDistractionFree: get( 'core', 'distractionFree' ), isPreviewMode: editorSettings.__unstableIsPreviewMode, - previousShortcut: select( - keyboardShortcutsStore - ).getAllShortcutKeyCombinations( 'core/editor/previous-region' ), - nextShortcut: select( - keyboardShortcutsStore - ).getAllShortcutKeyCombinations( 'core/editor/next-region' ), showBlockBreadcrumbs: get( 'core', 'showBlockBreadcrumbs' ), - // translators: Default label for the Document in the Block Breadcrumb. - documentLabel: postTypeLabel || _x( 'Document', 'noun' ), - blockEditorMode: - select( blockEditorStore ).__unstableGetEditorMode(), + documentLabel: + // translators: Default label for the Document in the Block Breadcrumb. + postTypeLabel || _x( 'Document', 'noun, breadcrumb' ), + isZoomOut: _isZoomOut(), }; }, [] ); const isLargeViewport = useViewportMatch( 'medium' ); @@ -119,7 +112,6 @@ export default function EditorInterface( { return ( ) @@ -229,10 +221,6 @@ export default function EditorInterface( { ) : undefined } - shortcuts={ { - previous: previousShortcut, - next: nextShortcut, - } } /> ); } diff --git a/packages/editor/src/components/entities-saved-states/index.js b/packages/editor/src/components/entities-saved-states/index.js index 8c0d59573a44d9..ba84ef2b392f5b 100644 --- a/packages/editor/src/components/entities-saved-states/index.js +++ b/packages/editor/src/components/entities-saved-states/index.js @@ -128,11 +128,21 @@ export function EntitiesSavedStatesExtensible( { aria-describedby={ renderDialog ? dialogDescription : undefined } > + + { __( 'Cancel' ) } + @@ -147,14 +157,6 @@ export function EntitiesSavedStatesExtensible( { > { saveLabel } - - { __( 'Cancel' ) } -
diff --git a/packages/editor/src/components/entities-saved-states/style.scss b/packages/editor/src/components/entities-saved-states/style.scss index 981a0d92e5ff6b..e2c320678c322a 100644 --- a/packages/editor/src/components/entities-saved-states/style.scss +++ b/packages/editor/src/components/entities-saved-states/style.scss @@ -1,8 +1,8 @@ .entities-saved-states__panel-header { box-sizing: border-box; background: $white; - padding-left: $grid-unit-10; - padding-right: $grid-unit-10; + padding-left: $grid-unit-20; + padding-right: $grid-unit-20; height: $header-height; border-bottom: $border-width solid $gray-300; } diff --git a/packages/editor/src/components/global-styles-provider/index.js b/packages/editor/src/components/global-styles-provider/index.js index 6f2d8177056cb1..3f3a3389801eb6 100644 --- a/packages/editor/src/components/global-styles-provider/index.js +++ b/packages/editor/src/components/global-styles-provider/index.js @@ -56,13 +56,27 @@ function useGlobalStylesUserConfig() { select( coreStore ).__experimentalGetCurrentGlobalStylesId(); let record; - const userCanEditGlobalStyles = canUser( 'update', { - kind: 'root', - name: 'globalStyles', - id: _globalStylesId, - } ); - if ( _globalStylesId ) { + // We want the global styles ID request to finish before triggering + // the OPTIONS request for user capabilities, otherwise it will + // fetch `/wp/v2/global-styles` instead of + // `/wp/v2/global-styles/{id}`! + // Please adjust the preloaded requests if this changes! + const userCanEditGlobalStyles = _globalStylesId + ? canUser( 'update', { + kind: 'root', + name: 'globalStyles', + id: _globalStylesId, + } ) + : null; + + if ( + _globalStylesId && + // We want the OPTIONS request for user capabilities to finish + // before getting the records, otherwise we'll fetch both! + typeof userCanEditGlobalStyles === 'boolean' + ) { + // Please adjust the preloaded requests if this changes! if ( userCanEditGlobalStyles ) { record = getEditedEntityRecord( 'root', diff --git a/packages/editor/src/components/header/index.js b/packages/editor/src/components/header/index.js index fb034ba8bb8574..eb0dd68647f56f 100644 --- a/packages/editor/src/components/header/index.js +++ b/packages/editor/src/components/header/index.js @@ -57,6 +57,7 @@ function Header( { isPublishSidebarOpened, showIconLabels, hasFixedToolbar, + hasBlockSelection, isNestedEntity, } = useSelect( ( select ) => { const { get: getPreference } = select( preferencesStore ); @@ -72,6 +73,8 @@ function Header( { isPublishSidebarOpened: _isPublishSidebarOpened(), showIconLabels: getPreference( 'core', 'showIconLabels' ), hasFixedToolbar: getPreference( 'core', 'fixedToolbar' ), + hasBlockSelection: + !! select( blockEditorStore ).getBlockSelectionStart(), isNestedEntity: !! getEditorSettings().onNavigateToPreviousEntityRecord, isZoomedOutView: __unstableGetEditorMode() === 'zoom-out', @@ -81,7 +84,9 @@ function Header( { const [ isBlockToolsCollapsed, setIsBlockToolsCollapsed ] = useState( true ); - const hasCenter = isBlockToolsCollapsed && ! isTooNarrowForDocumentBar; + const hasCenter = + ( ! hasBlockSelection || isBlockToolsCollapsed ) && + ! isTooNarrowForDocumentBar; const hasBackButton = useHasBackButton(); // The edit-post-header classname is only kept for backward compatibilty @@ -134,6 +139,7 @@ function Header( { // when the publish sidebar has been closed. ) } + - { isEditorIframed && isWideViewport && } + { isEditorIframed && isWideViewport && ( + + ) } { ( isWideViewport || ! showIconLabels ) && ( diff --git a/packages/editor/src/components/header/style.scss b/packages/editor/src/components/header/style.scss index 8712121fff3ea6..4259ef4b8a2084 100644 --- a/packages/editor/src/components/header/style.scss +++ b/packages/editor/src/components/header/style.scss @@ -130,6 +130,7 @@ // ... and display labels. &::after { content: attr(aria-label); + white-space: nowrap; } &[aria-disabled="true"] { background-color: transparent; diff --git a/packages/editor/src/components/page-attributes/parent.js b/packages/editor/src/components/page-attributes/parent.js index 0f19ffcdd5daa4..17395589cd313b 100644 --- a/packages/editor/src/components/page-attributes/parent.js +++ b/packages/editor/src/components/page-attributes/parent.js @@ -212,8 +212,10 @@ function PostParentToggle( { isOpen, onClick } ) { className="editor-post-parent__panel-toggle" variant="tertiary" aria-expanded={ isOpen } - // translators: %s: Current post parent. - aria-label={ sprintf( __( 'Change parent: %s' ), parentTitle ) } + aria-label={ + // translators: %s: Current post parent. + sprintf( __( 'Change parent: %s' ), parentTitle ) + } onClick={ onClick } > { parentTitle } @@ -261,9 +263,9 @@ export function ParentRow() {
{ createInterpolateElement( sprintf( - /* translators: %1$s The home URL of the WordPress installation without the scheme. */ + /* translators: %s: The home URL of the WordPress installation without the scheme. */ __( - 'Child pages inherit characteristics from their parent, such as URL structure. For instance, if "Pricing" is a child of "Services", its URL would be %1$s/services/pricing.' + 'Child pages inherit characteristics from their parent, such as URL structure. For instance, if "Pricing" is a child of "Services", its URL would be %s/services/pricing.' ), filterURLForDisplay( homeUrl ).replace( /([/.])/g, diff --git a/packages/editor/src/components/plugin-sidebar/index.js b/packages/editor/src/components/plugin-sidebar/index.js index b9c0177e30fc42..56a954cadffb69 100644 --- a/packages/editor/src/components/plugin-sidebar/index.js +++ b/packages/editor/src/components/plugin-sidebar/index.js @@ -3,7 +3,6 @@ */ import { useSelect } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; -import { store as keyboardShortcutsStore } from '@wordpress/keyboard-shortcuts'; import { ComplementaryArea } from '@wordpress/interface'; /** @@ -77,12 +76,9 @@ import { store as editorStore } from '../../store'; * ``` */ export default function PluginSidebar( { className, ...props } ) { - const { postTitle, shortcut } = useSelect( ( select ) => { + const { postTitle } = useSelect( ( select ) => { return { postTitle: select( editorStore ).getEditedPostAttribute( 'title' ), - shortcut: select( - keyboardShortcutsStore - ).getShortcutRepresentation( 'core/editor/toggle-sidebar' ), }; }, [] ); return ( @@ -91,7 +87,6 @@ export default function PluginSidebar( { className, ...props } ) { className="editor-sidebar" smallScreenTitle={ postTitle || __( '(no title)' ) } scope="core" - toggleShortcut={ shortcut } { ...props } /> ); diff --git a/packages/editor/src/components/post-author/panel.js b/packages/editor/src/components/post-author/panel.js index a3c53aa417163e..6c6a51918902dc 100644 --- a/packages/editor/src/components/post-author/panel.js +++ b/packages/editor/src/components/post-author/panel.js @@ -25,8 +25,10 @@ function PostAuthorToggle( { isOpen, onClick } ) { className="editor-post-author__panel-toggle" variant="tertiary" aria-expanded={ isOpen } - // translators: %s: Current post link. - aria-label={ sprintf( __( 'Change author: %s' ), authorName ) } + aria-label={ + // translators: %s: Author name. + sprintf( __( 'Change author: %s' ), authorName ) + } onClick={ onClick } > { authorName } diff --git a/packages/editor/src/components/post-content-information/index.js b/packages/editor/src/components/post-content-information/index.js index 569339ef40c8b9..17a43b1ba12962 100644 --- a/packages/editor/src/components/post-content-information/index.js +++ b/packages/editor/src/components/post-content-information/index.js @@ -70,7 +70,7 @@ export default function PostContentInformation() { readingTime <= 1 ? __( '1 minute' ) : sprintf( - // translators: %s: the number of minutes to read the post. + /* translators: %s: the number of minutes to read the post. */ _n( '%s minute', '%s minutes', readingTime ), readingTime.toLocaleString() ); diff --git a/packages/editor/src/components/post-last-revision/index.js b/packages/editor/src/components/post-last-revision/index.js index c0ce37198c951d..fd68f9703cb4e2 100644 --- a/packages/editor/src/components/post-last-revision/index.js +++ b/packages/editor/src/components/post-last-revision/index.js @@ -44,7 +44,7 @@ function PostLastRevision() { icon={ backup } iconPosition="right" text={ sprintf( - /* translators: %s: number of revisions */ + /* translators: %s: number of revisions. */ __( 'Revisions (%s)' ), revisionsCount ) } diff --git a/packages/editor/src/components/post-publish-button/index.js b/packages/editor/src/components/post-publish-button/index.js index 0460b27b616e81..71e18a4d6a9c82 100644 --- a/packages/editor/src/components/post-publish-button/index.js +++ b/packages/editor/src/components/post-publish-button/index.js @@ -2,7 +2,7 @@ * WordPress dependencies */ import { Button } from '@wordpress/components'; -import { Component, createRef } from '@wordpress/element'; +import { Component } from '@wordpress/element'; import { withSelect, withDispatch } from '@wordpress/data'; import { compose } from '@wordpress/compose'; @@ -17,7 +17,6 @@ const noop = () => {}; export class PostPublishButton extends Component { constructor( props ) { super( props ); - this.buttonNode = createRef(); this.createOnClick = this.createOnClick.bind( this ); this.closeEntitiesSavedStates = @@ -28,21 +27,6 @@ export class PostPublishButton extends Component { }; } - componentDidMount() { - if ( this.props.focusOnMount ) { - // This timeout is necessary to make sure the `useEffect` hook of - // `useFocusReturn` gets the correct element (the button that opens the - // PostPublishPanel) otherwise it will get this button. - this.timeoutID = setTimeout( () => { - this.buttonNode.current.focus(); - }, 0 ); - } - } - - componentWillUnmount() { - clearTimeout( this.timeoutID ); - } - createOnClick( callback ) { return ( ...args ) => { const { hasNonPostEntityChanges, setEntitiesSavedStatesCallback } = @@ -182,7 +166,6 @@ export class PostPublishButton extends Component { return ( <>
+
+ +
) }
diff --git a/packages/editor/src/components/post-publish-panel/maybe-upload-media.js b/packages/editor/src/components/post-publish-panel/maybe-upload-media.js index 8c8d757c583399..32ea69c425e0b5 100644 --- a/packages/editor/src/components/post-publish-panel/maybe-upload-media.js +++ b/packages/editor/src/components/post-publish-panel/maybe-upload-media.js @@ -98,8 +98,8 @@ function Image( { clientId, alt, url } ) { animate={ { opacity: 1 } } exit={ { opacity: 0, scale: 0 } } style={ { - width: '36px', - height: '36px', + width: '32px', + height: '32px', objectFit: 'cover', borderRadius: '2px', cursor: 'pointer', @@ -256,7 +256,7 @@ export default function MaybeUploadMediaPanel() { ) : (
@@ -586,24 +586,24 @@ exports[`PostPublishPanel should render the pre-publish panel if the post is not class="editor-post-publish-panel__header" >
@@ -783,24 +783,24 @@ exports[`PostPublishPanel should render the spinner if the post is being saved 1 class="editor-post-publish-panel__header" >
diff --git a/packages/editor/src/components/post-schedule/label.js b/packages/editor/src/components/post-schedule/label.js index 89e7d02d69ce7f..f6cf3811db7916 100644 --- a/packages/editor/src/components/post-schedule/label.js +++ b/packages/editor/src/components/post-schedule/label.js @@ -48,7 +48,7 @@ export function getFullPostScheduleLabel( dateAttribute ) { const timezoneAbbreviation = getTimezoneAbbreviation(); const formattedDate = dateI18n( - // translators: If using a space between 'g:i' and 'a', use a non-breaking space. + // translators: Use a non-breaking space between 'g:i' and 'a' if appropriate. _x( 'F j, Y g:i\xa0a', 'post schedule full date format' ), date ); diff --git a/packages/editor/src/components/post-taxonomies/flat-term-selector.js b/packages/editor/src/components/post-taxonomies/flat-term-selector.js index 5f581e898c953e..cd9377766af503 100644 --- a/packages/editor/src/components/post-taxonomies/flat-term-selector.js +++ b/packages/editor/src/components/post-taxonomies/flat-term-selector.js @@ -2,8 +2,12 @@ * WordPress dependencies */ import { __, _x, sprintf } from '@wordpress/i18n'; -import { useEffect, useMemo, useState } from '@wordpress/element'; -import { FormTokenField, withFilters } from '@wordpress/components'; +import { Fragment, useEffect, useMemo, useState } from '@wordpress/element'; +import { + FormTokenField, + withFilters, + __experimentalVStack as VStack, +} from '@wordpress/components'; import { useSelect, useDispatch } from '@wordpress/data'; import deprecated from '@wordpress/deprecated'; import { store as coreStore } from '@wordpress/core-data'; @@ -53,6 +57,13 @@ const termNamesToIds = ( names, terms ) => { .filter( ( id ) => id !== undefined ); }; +const Wrapper = ( { children, __nextHasNoMarginBottom } ) => + __nextHasNoMarginBottom ? ( + { children } + ) : ( + { children } + ); + /** * Renders a flat term selector component. * @@ -289,7 +300,7 @@ export function FlatTermSelector( { slug, __nextHasNoMarginBottom } ) { ); return ( - <> + - + ); } diff --git a/packages/editor/src/components/post-taxonomies/hierarchical-term-selector.js b/packages/editor/src/components/post-taxonomies/hierarchical-term-selector.js index 071453f4f3f626..e3e9e866df37ec 100644 --- a/packages/editor/src/components/post-taxonomies/hierarchical-term-selector.js +++ b/packages/editor/src/components/post-taxonomies/hierarchical-term-selector.js @@ -310,7 +310,7 @@ export function HierarchicalTermSelector( { slug } ) { const defaultName = slug === 'category' ? __( 'Category' ) : __( 'Term' ); const termAddedMessage = sprintf( - /* translators: %s: taxonomy name */ + /* translators: %s: term name. */ _x( '%s added', 'term' ), taxonomy?.labels?.singular_name ?? defaultName ); @@ -341,7 +341,7 @@ export function HierarchicalTermSelector( { slug } ) { const resultCount = getResultCount( newFilteredTermsTree ); const resultsFoundMessage = sprintf( - /* translators: %d: number of results */ + /* translators: %d: number of results. */ _n( '%d result found.', '%d results found.', resultCount ), resultCount ); diff --git a/packages/editor/src/components/post-url/panel.js b/packages/editor/src/components/post-url/panel.js index aca36566c04727..d6d6a5817ab969 100644 --- a/packages/editor/src/components/post-url/panel.js +++ b/packages/editor/src/components/post-url/panel.js @@ -86,8 +86,10 @@ function PostURLToggle( { isOpen, onClick } ) { className="editor-post-url__panel-toggle" variant="tertiary" aria-expanded={ isOpen } - // translators: %s: Current post link. - aria-label={ sprintf( __( 'Change link: %s' ), decodedSlug ) } + aria-label={ + // translators: %s: Current post link. + sprintf( __( 'Change link: %s' ), decodedSlug ) + } onClick={ onClick } > diff --git a/packages/editor/src/components/preferences-modal/index.js b/packages/editor/src/components/preferences-modal/index.js index a8cfd8245522cd..7ea7ea456ce28e 100644 --- a/packages/editor/src/components/preferences-modal/index.js +++ b/packages/editor/src/components/preferences-modal/index.js @@ -36,25 +36,40 @@ const { } = unlock( preferencesPrivateApis ); export default function EditorPreferencesModal( { extraSections = {} } ) { + const isActive = useSelect( ( select ) => { + return select( interfaceStore ).isModalActive( 'editor/preferences' ); + }, [] ); + const { closeModal } = useDispatch( interfaceStore ); + + if ( ! isActive ) { + return null; + } + + // Please wrap all contents inside PreferencesModalContents to prevent all + // hooks from executing when the modal is not open. + return ( + + + + ); +} + +function PreferencesModalContents( { extraSections = {} } ) { const isLargeViewport = useViewportMatch( 'medium' ); - const { isActive, showBlockBreadcrumbsOption } = useSelect( + const showBlockBreadcrumbsOption = useSelect( ( select ) => { const { getEditorSettings } = select( editorStore ); const { get } = select( preferencesStore ); - const { isModalActive } = select( interfaceStore ); const isRichEditingEnabled = getEditorSettings().richEditingEnabled; const isDistractionFreeEnabled = get( 'core', 'distractionFree' ); - return { - showBlockBreadcrumbsOption: - ! isDistractionFreeEnabled && - isLargeViewport && - isRichEditingEnabled, - isActive: isModalActive( 'editor/preferences' ), - }; + return ( + ! isDistractionFreeEnabled && + isLargeViewport && + isRichEditingEnabled + ); }, [ isLargeViewport ] ); - const { closeModal } = useDispatch( interfaceStore ); const { setIsListViewOpened, setIsInserterOpened } = useDispatch( editorStore ); const { set: setPreference } = useDispatch( preferencesStore ); @@ -330,13 +345,5 @@ export default function EditorPreferencesModal( { extraSections = {} } ) { ] ); - if ( ! isActive ) { - return null; - } - - return ( - - - - ); + return ; } diff --git a/packages/editor/src/components/preferences-modal/test/index.js b/packages/editor/src/components/preferences-modal/test/index.js index 01ac1a88fbe7d8..70102eea40f2ee 100644 --- a/packages/editor/src/components/preferences-modal/test/index.js +++ b/packages/editor/src/components/preferences-modal/test/index.js @@ -19,7 +19,7 @@ jest.mock( '@wordpress/compose/src/hooks/use-viewport-match', () => jest.fn() ); describe( 'EditPostPreferencesModal', () => { it( 'should not render when the modal is not active', () => { - useSelect.mockImplementation( () => [ false, false, false ] ); + useSelect.mockImplementation( () => false ); render( ); expect( screen.queryByRole( 'dialog', { name: 'Preferences' } ) diff --git a/packages/editor/src/components/preview-dropdown/index.js b/packages/editor/src/components/preview-dropdown/index.js index ecc5bc610a3027..0fbb2beb62665e 100644 --- a/packages/editor/src/components/preview-dropdown/index.js +++ b/packages/editor/src/components/preview-dropdown/index.js @@ -26,7 +26,9 @@ import { ActionItem } from '@wordpress/interface'; * Internal dependencies */ import { store as editorStore } from '../../store'; +import { store as blockEditorStore } from '@wordpress/block-editor'; import PostPreviewButton from '../post-preview-button'; +import { unlock } from '../../lock-unlock'; export default function PreviewDropdown( { forceIsAutosaveable, disabled } ) { const { deviceType, homeUrl, isTemplate, isViewable, showIconLabels } = @@ -44,6 +46,14 @@ export default function PreviewDropdown( { forceIsAutosaveable, disabled } ) { }; }, [] ); const { setDeviceType } = useDispatch( editorStore ); + const { __unstableSetEditorMode } = useDispatch( blockEditorStore ); + const { resetZoomLevel } = unlock( useDispatch( blockEditorStore ) ); + + const handleDevicePreviewChange = ( newDeviceType ) => { + setDeviceType( newDeviceType ); + __unstableSetEditorMode( 'edit' ); + resetZoomLevel(); + }; const isMobile = useViewportMatch( 'medium', '<' ); if ( isMobile ) { @@ -113,7 +123,7 @@ export default function PreviewDropdown( { forceIsAutosaveable, disabled } ) { { isTemplate && ( diff --git a/packages/editor/src/components/provider/index.js b/packages/editor/src/components/provider/index.js index 11b1478d58434a..e266ac83226dab 100644 --- a/packages/editor/src/components/provider/index.js +++ b/packages/editor/src/components/provider/index.js @@ -13,6 +13,7 @@ import { BlockEditorProvider, BlockContextProvider, privateApis as blockEditorPrivateApis, + store as blockEditorStore, } from '@wordpress/block-editor'; import { store as noticesStore } from '@wordpress/notices'; import { privateApis as editPatternsPrivateApis } from '@wordpress/patterns'; @@ -164,50 +165,59 @@ export const ExperimentalEditorProvider = withRegistryProvider( BlockEditorProviderComponent = ExperimentalBlockEditorProvider, __unstableTemplate: template, } ) => { - const { editorSettings, selection, isReady, mode, postTypes } = - useSelect( ( select ) => { - const { - getEditorSettings, - getEditorSelection, - getRenderingMode, - __unstableIsEditorReady, - } = select( editorStore ); - const { getPostTypes } = select( coreStore ); + const { editorSettings, selection, isReady, mode, postTypeEntities } = + useSelect( + ( select ) => { + const { + getEditorSettings, + getEditorSelection, + getRenderingMode, + __unstableIsEditorReady, + } = select( editorStore ); + const { getEntitiesConfig } = select( coreStore ); + + return { + editorSettings: getEditorSettings(), + isReady: __unstableIsEditorReady(), + mode: getRenderingMode(), + selection: getEditorSelection(), + postTypeEntities: + post.type === 'wp_template' + ? getEntitiesConfig( 'postType' ) + : null, + }; + }, + [ post.type ] + ); + + const isZoomOut = useSelect( ( select ) => { + const { __unstableGetEditorMode } = unlock( + select( blockEditorStore ) + ); + + return __unstableGetEditorMode() === 'zoom-out'; + } ); - return { - editorSettings: getEditorSettings(), - isReady: __unstableIsEditorReady(), - mode: getRenderingMode(), - selection: getEditorSelection(), - postTypes: getPostTypes( { per_page: -1 } ), - }; - }, [] ); const shouldRenderTemplate = !! template && mode !== 'post-only'; const rootLevelPost = shouldRenderTemplate ? template : post; const defaultBlockContext = useMemo( () => { const postContext = {}; - // If it is a template, try to inherit the post type from the slug. + // If it is a template, try to inherit the post type from the name. if ( post.type === 'wp_template' ) { - if ( ! post.is_custom ) { - const [ kind ] = post.slug.split( '-' ); - switch ( kind ) { - case 'page': - postContext.postType = 'page'; - break; - case 'single': - // Infer the post type from the slug. - const postTypesSlugs = - postTypes?.map( ( entity ) => entity.slug ) || - []; - const match = post.slug.match( - `^single-(${ postTypesSlugs.join( - '|' - ) })(?:-.+)?$` - ); - if ( match ) { - postContext.postType = match[ 1 ]; - } - break; + if ( post.slug === 'page' ) { + postContext.postType = 'page'; + } else if ( post.slug === 'single' ) { + postContext.postType = 'post'; + } else if ( post.slug.split( '-' )[ 0 ] === 'single' ) { + // If the slug is single-{postType}, infer the post type from the name. + const postTypeNames = + postTypeEntities?.map( ( entity ) => entity.name ) || + []; + const match = post.slug.match( + `^single-(${ postTypeNames.join( '|' ) })(?:-.+)?$` + ); + if ( match ) { + postContext.postType = match[ 1 ]; } } } else if ( @@ -229,9 +239,10 @@ export const ExperimentalEditorProvider = withRegistryProvider( shouldRenderTemplate, post.id, post.type, + post.slug, rootLevelPost.type, rootLevelPost.slug, - postTypes, + postTypeEntities, ] ); const { id, type } = rootLevelPost; const blockEditorSettings = useBlockEditorSettings( @@ -331,9 +342,13 @@ export const ExperimentalEditorProvider = withRegistryProvider( { children } { ! settings.__unstableIsPreviewMode && ( <> - - - + { ! isZoomOut && ( + <> + + + + + ) } { mode === 'template-locked' && ( ) } diff --git a/packages/editor/src/components/sidebar/header.js b/packages/editor/src/components/sidebar/header.js index fc4d44ba9e2958..ed2f7f89fe6e7a 100644 --- a/packages/editor/src/components/sidebar/header.js +++ b/packages/editor/src/components/sidebar/header.js @@ -20,8 +20,9 @@ const SidebarHeader = ( _, ref ) => { const { getPostTypeLabel } = select( editorStore ); return { - // translators: Default label for the Document sidebar tab, not selected. - documentLabel: getPostTypeLabel() || _x( 'Document', 'noun' ), + documentLabel: + // translators: Default label for the Document sidebar tab, not selected. + getPostTypeLabel() || _x( 'Document', 'noun, sidebar' ), }; }, [] ); diff --git a/packages/editor/src/components/sidebar/index.js b/packages/editor/src/components/sidebar/index.js index efca709e0eec75..601bcd8f311bb8 100644 --- a/packages/editor/src/components/sidebar/index.js +++ b/packages/editor/src/components/sidebar/index.js @@ -13,7 +13,7 @@ import { useEffect, useRef, } from '@wordpress/element'; -import { isRTL, __ } from '@wordpress/i18n'; +import { isRTL, __, _x } from '@wordpress/i18n'; import { drawerLeft, drawerRight } from '@wordpress/icons'; import { store as keyboardShortcutsStore } from '@wordpress/keyboard-shortcuts'; import { privateApis as componentsPrivateApis } from '@wordpress/components'; @@ -101,8 +101,10 @@ const SidebarContent = ( { // see https://github.com/WordPress/gutenberg/pull/55360#pullrequestreview-1737671049 className="editor-sidebar__panel" headerClassName="editor-sidebar__panel-tabs" - /* translators: button label text should, if possible, be under 16 characters. */ - title={ __( 'Settings' ) } + title={ + /* translators: button label text should, if possible, be under 16 characters. */ + _x( 'Settings', 'sidebar button label' ) + } toggleShortcut={ keyboardShortcut } icon={ isRTL() ? drawerLeft : drawerRight } isActiveByDefault={ SIDEBAR_ACTIVE_BY_DEFAULT } diff --git a/packages/editor/src/components/sidebar/post-summary.js b/packages/editor/src/components/sidebar/post-summary.js index 72f1080770fd9d..3539f7ba964ec7 100644 --- a/packages/editor/src/components/sidebar/post-summary.js +++ b/packages/editor/src/components/sidebar/post-summary.js @@ -87,11 +87,11 @@ export default function PostSummary( { onActionPerformed } ) { + { fills } - { fills } ) } diff --git a/packages/editor/src/components/text-editor/index.js b/packages/editor/src/components/text-editor/index.js index fa0688859b5a69..6997c66826a12d 100644 --- a/packages/editor/src/components/text-editor/index.js +++ b/packages/editor/src/components/text-editor/index.js @@ -6,6 +6,7 @@ import { useDispatch, useSelect } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; import { store as keyboardShortcutsStore } from '@wordpress/keyboard-shortcuts'; import { useEffect, useRef } from '@wordpress/element'; +import { store as blockEditorStore } from '@wordpress/block-editor'; /** * Internal dependencies @@ -13,6 +14,7 @@ import { useEffect, useRef } from '@wordpress/element'; import { store as editorStore } from '../../store'; import PostTextEditor from '../post-text-editor'; import PostTitleRaw from '../post-title/post-title-raw'; +import { unlock } from '../../lock-unlock'; export default function TextEditor( { autoFocus = false } ) { const { switchEditorMode } = useDispatch( editorStore ); @@ -26,13 +28,20 @@ export default function TextEditor( { autoFocus = false } ) { }; }, [] ); + const { resetZoomLevel, __unstableSetEditorMode } = unlock( + useDispatch( blockEditorStore ) + ); + const titleRef = useRef(); useEffect( () => { + resetZoomLevel(); + __unstableSetEditorMode( 'edit' ); + if ( autoFocus ) { return; } titleRef?.current?.focus(); - }, [ autoFocus ] ); + }, [ autoFocus, resetZoomLevel, __unstableSetEditorMode ] ); return (
diff --git a/packages/editor/src/components/time-to-read/index.js b/packages/editor/src/components/time-to-read/index.js index a71a4b1dac8388..5d748abc3049cb 100644 --- a/packages/editor/src/components/time-to-read/index.js +++ b/packages/editor/src/components/time-to-read/index.js @@ -47,10 +47,10 @@ export default function TimeToRead() { } ) : createInterpolateElement( sprintf( - /* translators: %s is the number of minutes the post will take to read. */ + /* translators: %s: the number of minutes to read the post. */ _n( - '%d minute', - '%d minutes', + '%s minute', + '%s minutes', minutesToRead ), minutesToRead diff --git a/packages/editor/src/components/visual-editor/edit-template-blocks-notification.js b/packages/editor/src/components/visual-editor/edit-template-blocks-notification.js index 3ccbe79127c013..bacf1beb1abecd 100644 --- a/packages/editor/src/components/visual-editor/edit-template-blocks-notification.js +++ b/packages/editor/src/components/visual-editor/edit-template-blocks-notification.js @@ -61,7 +61,11 @@ export default function EditTemplateBlocksNotification( { contentRef } ) { ) { return; } - setIsDialogOpen( true ); + + if ( ! event.defaultPrevented ) { + event.preventDefault(); + setIsDialogOpen( true ); + } }; const canvas = contentRef.current; diff --git a/packages/editor/src/components/visual-editor/index.js b/packages/editor/src/components/visual-editor/index.js index 2ff115272d614b..d154e77b58003e 100644 --- a/packages/editor/src/components/visual-editor/index.js +++ b/packages/editor/src/components/visual-editor/index.js @@ -48,6 +48,7 @@ const { useLayoutStyles, ExperimentalBlockCanvas: BlockCanvas, useFlashEditableBlocks, + useZoomOutModeExit, } = unlock( blockEditorPrivateApis ); /** @@ -174,17 +175,19 @@ function VisualEditor( { hasRootPaddingAwareAlignments, themeHasDisabledLayoutStyles, themeSupportsLayout, - isZoomOutMode, + isZoomedOut, } = useSelect( ( select ) => { - const { getSettings, __unstableGetEditorMode } = - select( blockEditorStore ); + const { getSettings, isZoomOut: _isZoomOut } = unlock( + select( blockEditorStore ) + ); + const _settings = getSettings(); return { themeHasDisabledLayoutStyles: _settings.disableLayoutStyles, themeSupportsLayout: _settings.supportsLayout, hasRootPaddingAwareAlignments: _settings.__experimentalFeatures?.useRootPaddingAwareAlignments, - isZoomOutMode: __unstableGetEditorMode() === 'zoom-out', + isZoomedOut: _isZoomOut(), }; }, [] ); @@ -333,13 +336,14 @@ function VisualEditor( { useSelectNearestEditableBlock( { isEnabled: renderingMode === 'template-locked', } ), + useZoomOutModeExit(), ] ); const zoomOutProps = - isZoomOutMode && ! isTabletViewport + isZoomedOut && ! isTabletViewport ? { scale: 'default', - frameSize: '48px', + frameSize: '40px', } : {}; @@ -355,7 +359,7 @@ function VisualEditor( { // Disable resizing in mobile viewport. ! isMobileViewport && // Dsiable resizing in zoomed-out mode. - ! isZoomOutMode; + ! isZoomedOut; const shouldIframe = ! disableIframe || [ 'Tablet', 'Mobile' ].includes( deviceType ); @@ -363,7 +367,10 @@ function VisualEditor( { return [ ...( styles ?? [] ), { - css: `.is-root-container{display:flow-root;${ + // Ensures margins of children are contained so that the body background paints behind them. + // Otherwise, the background of html (when zoomed out) would show there and appear broken. It’s + // important mostly for post-only views yet conceivably an issue in templated views too. + css: `:where(.block-editor-iframe__body){display:flow-root;}.is-root-container{display:flow-root;${ // Some themes will have `min-height: 100vh` for the root container, // which isn't a requirement in auto resize mode. enableResizing ? 'min-height:0!important;' : '' diff --git a/packages/editor/src/components/zoom-out-toggle/index.js b/packages/editor/src/components/zoom-out-toggle/index.js index e8c7b1e50510ab..18d5fcdbe2de19 100644 --- a/packages/editor/src/components/zoom-out-toggle/index.js +++ b/packages/editor/src/components/zoom-out-toggle/index.js @@ -7,26 +7,45 @@ import { __ } from '@wordpress/i18n'; import { useDispatch, useSelect } from '@wordpress/data'; import { store as blockEditorStore } from '@wordpress/block-editor'; import { square as zoomOutIcon } from '@wordpress/icons'; +import { store as preferencesStore } from '@wordpress/preferences'; -const ZoomOutToggle = () => { - const { isZoomOutMode } = useSelect( ( select ) => ( { - isZoomOutMode: - select( blockEditorStore ).__unstableGetEditorMode() === 'zoom-out', +/** + * Internal dependencies + */ +import { unlock } from '../../lock-unlock'; + +const ZoomOutToggle = ( { disabled } ) => { + const { isZoomOut, showIconLabels } = useSelect( ( select ) => ( { + isZoomOut: unlock( select( blockEditorStore ) ).isZoomOut(), + showIconLabels: select( preferencesStore ).get( + 'core', + 'showIconLabels' + ), } ) ); - const { __unstableSetEditorMode } = useDispatch( blockEditorStore ); + const { resetZoomLevel, setZoomLevel, __unstableSetEditorMode } = unlock( + useDispatch( blockEditorStore ) + ); const handleZoomOut = () => { - __unstableSetEditorMode( isZoomOutMode ? 'edit' : 'zoom-out' ); + if ( isZoomOut ) { + resetZoomLevel(); + } else { + setZoomLevel( 50 ); + } + __unstableSetEditorMode( isZoomOut ? 'edit' : 'zoom-out' ); }; return (
* ``` * Note that, in the previous example, the directive callback gets the path - * value (`state.alert`) from the directive entry with suffix `default`. A + * value (`state.alert`) from the directive entry with suffix `null`. A * custom suffix can also be specified by appending `--` to the directive * attribute, followed by the suffix, like in the following HTML snippet: * @@ -253,7 +273,9 @@ const Directives = ( { // element ref, state and props. const scope = useRef< Scope >( {} as Scope ).current; scope.evaluate = useCallback( getEvaluate( { scope } ), [] ); - scope.context = useContext( context ); + const { client, server } = useContext( context ); + scope.context = client; + scope.serverContext = server; /* eslint-disable react-hooks/rules-of-hooks */ scope.ref = previousScope?.ref || useRef( null ); /* eslint-enable react-hooks/rules-of-hooks */ @@ -305,9 +327,7 @@ options.vnode = ( vnode: VNode< any > ) => { const props = vnode.props; const directives = props.__directives; if ( directives.key ) { - vnode.key = directives.key.find( - ( { suffix } ) => suffix === 'default' - ).value; + vnode.key = directives.key.find( isDefaultDirectiveSuffix ).value; } delete props.__directives; const priorityLevels = getPriorityLevels( directives ); diff --git a/packages/interactivity/src/index.ts b/packages/interactivity/src/index.ts index 336c2a97226db7..9d013e4e744ed5 100644 --- a/packages/interactivity/src/index.ts +++ b/packages/interactivity/src/index.ts @@ -16,8 +16,8 @@ import { getNamespace } from './namespaces'; import { parseServerData, populateServerData } from './store'; import { proxifyState } from './proxies'; -export { store, getConfig } from './store'; -export { getContext, getElement } from './scopes'; +export { store, getConfig, getServerState } from './store'; +export { getContext, getServerContext, getElement } from './scopes'; export { withScope, useWatch, diff --git a/packages/interactivity/src/proxies/state.ts b/packages/interactivity/src/proxies/state.ts index ec49c4b27c4adb..d4d573acba8847 100644 --- a/packages/interactivity/src/proxies/state.ts +++ b/packages/interactivity/src/proxies/state.ts @@ -46,6 +46,8 @@ const proxyToProps: WeakMap< export const hasPropSignal = ( proxy: object, key: string ) => proxyToProps.has( proxy ) && proxyToProps.get( proxy )!.has( key ); +const readOnlyProxies = new WeakSet(); + /** * Returns the {@link PropSignal | `PropSignal`} instance associated with the * specified prop in the passed proxy. @@ -77,8 +79,11 @@ const getPropSignal = ( if ( get ) { prop.setGetter( get ); } else { + const readOnly = readOnlyProxies.has( proxy ); prop.setValue( - shouldProxy( value ) ? proxifyState( ns, value ) : value + shouldProxy( value ) + ? proxifyState( ns, value, { readOnly } ) + : value ); } } @@ -148,6 +153,9 @@ const stateHandlers: ProxyHandler< object > = { value: unknown, receiver: object ): boolean { + if ( readOnlyProxies.has( receiver ) ) { + return false; + } setNamespace( getNamespaceFromProxy( receiver ) ); try { return Reflect.set( target, key, value, receiver ); @@ -161,6 +169,10 @@ const stateHandlers: ProxyHandler< object > = { key: string, desc: PropertyDescriptor ): boolean { + if ( readOnlyProxies.has( getProxyFromObject( target )! ) ) { + return false; + } + const isNew = ! ( key in target ); const result = Reflect.defineProperty( target, key, desc ); @@ -199,6 +211,10 @@ const stateHandlers: ProxyHandler< object > = { }, deleteProperty( target: object, key: string ): boolean { + if ( readOnlyProxies.has( getProxyFromObject( target )! ) ) { + return false; + } + const result = Reflect.deleteProperty( target, key ); if ( result ) { @@ -230,8 +246,10 @@ const stateHandlers: ProxyHandler< object > = { * Returns the proxy associated with the given state object, creating it if it * does not exist. * - * @param namespace The namespace that will be associated to this proxy. - * @param obj The object to proxify. + * @param namespace The namespace that will be associated to this proxy. + * @param obj The object to proxify. + * @param options Options. + * @param options.readOnly Read-only. * * @throws Error if the object cannot be proxified. Use {@link shouldProxy} to * check if a proxy can be created for a specific object. @@ -240,8 +258,15 @@ const stateHandlers: ProxyHandler< object > = { */ export const proxifyState = < T extends object >( namespace: string, - obj: T -): T => createProxy( namespace, obj, stateHandlers ) as T; + obj: T, + options?: { readOnly?: boolean } +): T => { + const proxy = createProxy( namespace, obj, stateHandlers ) as T; + if ( options?.readOnly ) { + readOnlyProxies.add( proxy ); + } + return proxy; +}; /** * Reads the value of the specified property without subscribing to it. @@ -275,50 +300,64 @@ const deepMergeRecursive = ( source: any, override: boolean = true ) => { - if ( isPlainObject( target ) && isPlainObject( source ) ) { - let hasNewKeys = false; - for ( const key in source ) { - const isNew = ! ( key in target ); - hasNewKeys = hasNewKeys || isNew; + if ( ! ( isPlainObject( target ) && isPlainObject( source ) ) ) { + return; + } - const desc = Object.getOwnPropertyDescriptor( source, key ); - if ( - typeof desc?.get === 'function' || - typeof desc?.set === 'function' - ) { - if ( override || isNew ) { - Object.defineProperty( target, key, { - ...desc, - configurable: true, - enumerable: true, - } ); - - const proxy = getProxyFromObject( target ); - if ( desc?.get && proxy && hasPropSignal( proxy, key ) ) { - const propSignal = getPropSignal( proxy, key ); - propSignal.setGetter( desc.get ); - } - } - } else if ( isPlainObject( source[ key ] ) ) { - if ( isNew ) { - target[ key ] = {}; - } + let hasNewKeys = false; - deepMergeRecursive( target[ key ], source[ key ], override ); - } else if ( override || isNew ) { - Object.defineProperty( target, key, desc! ); + for ( const key in source ) { + const isNew = ! ( key in target ); + hasNewKeys = hasNewKeys || isNew; - const proxy = getProxyFromObject( target ); - if ( desc?.value && proxy && hasPropSignal( proxy, key ) ) { - const propSignal = getPropSignal( proxy, key ); - propSignal.setValue( desc.value ); + const desc = Object.getOwnPropertyDescriptor( source, key )!; + const proxy = getProxyFromObject( target ); + const propSignal = + !! proxy && + hasPropSignal( proxy, key ) && + getPropSignal( proxy, key ); + + if ( + typeof desc.get === 'function' || + typeof desc.set === 'function' + ) { + if ( override || isNew ) { + Object.defineProperty( target, key, { + ...desc, + configurable: true, + enumerable: true, + } ); + if ( desc.get && propSignal ) { + propSignal.setGetter( desc.get ); + } + } + } else if ( isPlainObject( source[ key ] ) ) { + if ( isNew || ( override && ! isPlainObject( target[ key ] ) ) ) { + target[ key ] = {}; + if ( propSignal ) { + const ns = getNamespaceFromProxy( proxy ); + propSignal.setValue( + proxifyState( ns, target[ key ] as Object ) + ); } } + if ( isPlainObject( target[ key ] ) ) { + deepMergeRecursive( target[ key ], source[ key ], override ); + } + } else if ( override || isNew ) { + Object.defineProperty( target, key, desc ); + if ( propSignal ) { + const { value } = desc; + const ns = getNamespaceFromProxy( proxy ); + propSignal.setValue( + shouldProxy( value ) ? proxifyState( ns, value ) : value + ); + } } + } - if ( hasNewKeys && objToIterable.has( target ) ) { - objToIterable.get( target )!.value++; - } + if ( hasNewKeys && objToIterable.has( target ) ) { + objToIterable.get( target )!.value++; } }; diff --git a/packages/interactivity/src/proxies/test/context-proxy.ts b/packages/interactivity/src/proxies/test/context-proxy.ts index 306b3e4a8aa94f..1e4a969d0f9dc6 100644 --- a/packages/interactivity/src/proxies/test/context-proxy.ts +++ b/packages/interactivity/src/proxies/test/context-proxy.ts @@ -6,7 +6,7 @@ import { effect } from '@preact/signals'; /** * Internal dependencies */ -import { proxifyContext, proxifyState } from '../'; +import { proxifyContext, proxifyState, deepMerge } from '../'; describe( 'Interactivity API', () => { describe( 'context proxy', () => { @@ -277,6 +277,66 @@ describe( 'Interactivity API', () => { 'fromFallback', ] ); } ); + + it( 'should handle deeply nested properties that are initially undefined', () => { + const fallback: any = proxifyContext( + proxifyState( 'test', {} ), + {} + ); + const context: any = proxifyContext( + proxifyState( 'test', {} ), + fallback + ); + + let deepValue: any; + const spy = jest.fn( () => { + deepValue = context.a?.b?.c?.d; + } ); + effect( spy ); + + // Initial call, the deep value is undefined + expect( spy ).toHaveBeenCalledTimes( 1 ); + expect( deepValue ).toBeUndefined(); + + // Add a deeply nested object to the context + context.a = { b: { c: { d: 'test value' } } }; + + // The effect should be called again + expect( spy ).toHaveBeenCalledTimes( 2 ); + expect( deepValue ).toBe( 'test value' ); + + // Reading the value directly should also work + expect( context.a.b.c.d ).toBe( 'test value' ); + } ); + + it( 'should handle deeply nested properties that are initially undefined and merged with deepMerge', () => { + const fallbackState = proxifyState( 'test', {} ); + const fallback: any = proxifyContext( fallbackState, {} ); + const contextState = proxifyState( 'test', {} ); + const context: any = proxifyContext( contextState, fallback ); + + let deepValue: any; + const spy = jest.fn( () => { + deepValue = context.a?.b?.c?.d; + } ); + effect( spy ); + + // Initial call, the deep value is undefined + expect( spy ).toHaveBeenCalledTimes( 1 ); + expect( deepValue ).toBeUndefined(); + + // Use deepMerge to add a deeply nested object to the context + deepMerge( contextState, { + a: { b: { c: { d: 'test value' } } }, + } ); + + // The effect should be called again + expect( spy ).toHaveBeenCalledTimes( 2 ); + expect( deepValue ).toBe( 'test value' ); + + // Reading the value directly should also work + expect( context.a.b.c.d ).toBe( 'test value' ); + } ); } ); describe( 'proxifyContext', () => { diff --git a/packages/interactivity/src/proxies/test/deep-merge.ts b/packages/interactivity/src/proxies/test/deep-merge.ts index bf31c4b552133c..267e4850f9af91 100644 --- a/packages/interactivity/src/proxies/test/deep-merge.ts +++ b/packages/interactivity/src/proxies/test/deep-merge.ts @@ -379,7 +379,10 @@ describe( 'Interactivity API', () => { const target = proxifyState< any >( 'test', { a: 1, b: 2 } ); const source = { a: 1, b: 2, c: 3 }; - const spy = jest.fn( () => Object.keys( target ) ); + let keys: any; + const spy = jest.fn( () => { + keys = Object.keys( target ); + } ); effect( spy ); expect( spy ).toHaveBeenCalledTimes( 1 ); @@ -387,7 +390,115 @@ describe( 'Interactivity API', () => { deepMerge( target, source, false ); expect( spy ).toHaveBeenCalledTimes( 2 ); - expect( spy ).toHaveLastReturnedWith( [ 'a', 'b', 'c' ] ); + expect( keys ).toEqual( [ 'a', 'b', 'c' ] ); + } ); + + it( 'should handle deeply nested properties that are initially undefined', () => { + const target: any = proxifyState( 'test', {} ); + + let deepValue: any; + const spy = jest.fn( () => { + deepValue = target.a?.b?.c?.d; + } ); + effect( spy ); + + // Initial call, the deep value is undefined + expect( spy ).toHaveBeenCalledTimes( 1 ); + expect( deepValue ).toBeUndefined(); + + // Use deepMerge to add a deeply nested object to the target + deepMerge( target, { a: { b: { c: { d: 'test value' } } } } ); + + // The effect should be called again + expect( spy ).toHaveBeenCalledTimes( 2 ); + expect( deepValue ).toBe( 'test value' ); + + // Reading the value directly should also work + expect( target.a.b.c.d ).toBe( 'test value' ); + + // Modify the nested value + target.a.b.c.d = 'new test value'; + + // The effect should be called again + expect( spy ).toHaveBeenCalledTimes( 3 ); + expect( deepValue ).toBe( 'new test value' ); + } ); + + it( 'should overwrite values that become objects', () => { + const target: any = proxifyState( 'test', { message: 'hello' } ); + + let message: any; + const spy = jest.fn( () => ( message = target.message ) ); + effect( spy ); + + expect( spy ).toHaveBeenCalledTimes( 1 ); + expect( message ).toBe( 'hello' ); + + deepMerge( target, { + message: { content: 'hello', fontStyle: 'italic' }, + } ); + + expect( spy ).toHaveBeenCalledTimes( 2 ); + expect( message ).toEqual( { + content: 'hello', + fontStyle: 'italic', + } ); + + expect( target.message ).toEqual( { + content: 'hello', + fontStyle: 'italic', + } ); + } ); + + it( 'should not overwrite values that become objects if `override` is false', () => { + const target: any = proxifyState( 'test', { message: 'hello' } ); + + let message: any; + const spy = jest.fn( () => ( message = target.message ) ); + effect( spy ); + + expect( spy ).toHaveBeenCalledTimes( 1 ); + expect( message ).toBe( 'hello' ); + + deepMerge( + target, + { message: { content: 'hello', fontStyle: 'italic' } }, + false + ); + + expect( spy ).toHaveBeenCalledTimes( 1 ); + expect( message ).toBe( 'hello' ); + expect( target.message ).toBe( 'hello' ); + expect( target.message.content ).toBeUndefined(); + expect( target.message.fontStyle ).toBeUndefined(); + } ); + + it( 'should keep reactivity of arrays that are initially undefined', () => { + const target: any = proxifyState( 'test', {} ); + + let deepValue: any; + const spy = jest.fn( () => { + deepValue = target.array?.[ 0 ]; + } ); + effect( spy ); + + // Initial call, the deep value is undefined + expect( spy ).toHaveBeenCalledTimes( 1 ); + expect( deepValue ).toBeUndefined(); + + // Use deepMerge to add an array to the target + deepMerge( target, { array: [ 'value 1' ] } ); + + // The effect should be called again + expect( spy ).toHaveBeenCalledTimes( 2 ); + expect( deepValue ).toBe( 'value 1' ); + + // Modify the array value + target.array[ 0 ] = 'value 2'; + + // The effect should be called again + expect( spy ).toHaveBeenCalledTimes( 3 ); + expect( deepValue ).toBe( 'value 2' ); } ); } ); } ); diff --git a/packages/interactivity/src/proxies/test/state-proxy.ts b/packages/interactivity/src/proxies/test/state-proxy.ts index 92500189fc8309..4b0d2b0a708c3a 100644 --- a/packages/interactivity/src/proxies/test/state-proxy.ts +++ b/packages/interactivity/src/proxies/test/state-proxy.ts @@ -9,7 +9,7 @@ import { effect } from '@preact/signals'; /** * Internal dependencies */ -import { proxifyState, peek } from '../'; +import { proxifyState, peek, deepMerge } from '../'; import { setScope, resetScope, getContext, getElement } from '../../scopes'; import { setNamespace, resetNamespace } from '../../namespaces'; @@ -1265,5 +1265,202 @@ describe( 'Interactivity API', () => { expect( x ).toBe( undefined ); } ); } ); + + describe( 'read-only', () => { + it( "should not allow modifying a prop's value", () => { + const readOnlyState = proxifyState( + 'test', + { prop: 'value', nested: { prop: 'value' } }, + { readOnly: true } + ); + + expect( () => { + readOnlyState.prop = 'new value'; + } ).toThrow(); + expect( () => { + readOnlyState.nested.prop = 'new value'; + } ).toThrow(); + } ); + + it( 'should not allow modifying a prop descriptor', () => { + const readOnlyState = proxifyState( + 'test', + { prop: 'value', nested: { prop: 'value' } }, + { readOnly: true } + ); + + expect( () => { + Object.defineProperty( readOnlyState, 'prop', { + get: () => 'value from getter', + writable: true, + enumerable: false, + } ); + } ).toThrow(); + expect( () => { + Object.defineProperty( readOnlyState.nested, 'prop', { + get: () => 'value from getter', + writable: true, + enumerable: false, + } ); + } ).toThrow(); + } ); + + it( 'should not allow adding new props', () => { + const readOnlyState = proxifyState< any >( + 'test', + { prop: 'value', nested: { prop: 'value' } }, + { readOnly: true } + ); + + expect( () => { + readOnlyState.newProp = 'value'; + } ).toThrow(); + expect( () => { + readOnlyState.nested.newProp = 'value'; + } ).toThrow(); + } ); + + it( 'should not allow removing props', () => { + const readOnlyState = proxifyState< any >( + 'test', + { prop: 'value', nested: { prop: 'value' } }, + { readOnly: true } + ); + + expect( () => { + delete readOnlyState.prop; + } ).toThrow(); + expect( () => { + delete readOnlyState.nested.prop; + } ).toThrow(); + } ); + + it( 'should not allow adding items to an array', () => { + const readOnlyState = proxifyState( + 'test', + { array: [ 1, 2, 3 ], nested: { array: [ 1, 2, 3 ] } }, + { readOnly: true } + ); + + expect( () => readOnlyState.array.push( 4 ) ).toThrow(); + expect( () => readOnlyState.nested.array.push( 4 ) ).toThrow(); + } ); + + it( 'should not allow removing items from an array', () => { + const readOnlyState = proxifyState( + 'test', + { array: [ 1, 2, 3 ], nested: { array: [ 1, 2, 3 ] } }, + { readOnly: true } + ); + + expect( () => readOnlyState.array.pop() ).toThrow(); + expect( () => readOnlyState.nested.array.pop() ).toThrow(); + } ); + + it( 'should allow subscribing to prop changes', () => { + const readOnlyState = proxifyState( + 'test', + { + prop: 'value', + nested: { prop: 'value' }, + }, + { readOnly: true } + ); + + const spy1 = jest.fn( () => readOnlyState.prop ); + const spy2 = jest.fn( () => readOnlyState.nested.prop ); + + effect( spy1 ); + effect( spy2 ); + expect( spy1 ).toHaveBeenCalledTimes( 1 ); + expect( spy2 ).toHaveBeenCalledTimes( 1 ); + expect( spy1 ).toHaveLastReturnedWith( 'value' ); + expect( spy2 ).toHaveLastReturnedWith( 'value' ); + + deepMerge( readOnlyState, { prop: 'new value' } ); + + expect( spy1 ).toHaveBeenCalledTimes( 2 ); + expect( spy2 ).toHaveBeenCalledTimes( 1 ); + expect( spy1 ).toHaveLastReturnedWith( 'new value' ); + expect( spy2 ).toHaveLastReturnedWith( 'value' ); + + deepMerge( readOnlyState, { nested: { prop: 'new value' } } ); + + expect( spy1 ).toHaveBeenCalledTimes( 2 ); + expect( spy2 ).toHaveBeenCalledTimes( 2 ); + expect( spy1 ).toHaveLastReturnedWith( 'new value' ); + expect( spy2 ).toHaveLastReturnedWith( 'new value' ); + } ); + + it( 'should allow subscribing to new props', () => { + const readOnlyState = proxifyState< any >( + 'test', + { + prop: 'value', + nested: { prop: 'value' }, + }, + { readOnly: true } + ); + + const spy1 = jest.fn( () => readOnlyState.newProp ); + const spy2 = jest.fn( () => readOnlyState.nested.newProp ); + + effect( spy1 ); + effect( spy2 ); + expect( spy1 ).toHaveBeenCalledTimes( 1 ); + expect( spy2 ).toHaveBeenCalledTimes( 1 ); + expect( spy1 ).toHaveLastReturnedWith( undefined ); + expect( spy2 ).toHaveLastReturnedWith( undefined ); + + deepMerge( readOnlyState, { newProp: 'value' } ); + + expect( spy1 ).toHaveBeenCalledTimes( 2 ); + expect( spy2 ).toHaveBeenCalledTimes( 1 ); + expect( spy1 ).toHaveLastReturnedWith( 'value' ); + expect( spy2 ).toHaveLastReturnedWith( undefined ); + + deepMerge( readOnlyState, { nested: { newProp: 'value' } } ); + + expect( spy1 ).toHaveBeenCalledTimes( 2 ); + expect( spy2 ).toHaveBeenCalledTimes( 2 ); + expect( spy1 ).toHaveLastReturnedWith( 'value' ); + expect( spy2 ).toHaveLastReturnedWith( 'value' ); + } ); + + it( 'should allow subscribing to array changes', () => { + const readOnlyState = proxifyState< any >( + 'test', + { + array: [ 1, 2, 3 ], + nested: { array: [ 1, 2, 3 ] }, + }, + { readOnly: true } + ); + + const spy1 = jest.fn( () => readOnlyState.array[ 0 ] ); + const spy2 = jest.fn( () => readOnlyState.nested.array[ 0 ] ); + + effect( spy1 ); + effect( spy2 ); + expect( spy1 ).toHaveBeenCalledTimes( 1 ); + expect( spy2 ).toHaveBeenCalledTimes( 1 ); + expect( spy1 ).toHaveLastReturnedWith( 1 ); + expect( spy2 ).toHaveLastReturnedWith( 1 ); + + deepMerge( readOnlyState, { array: [ 4, 5, 6 ] } ); + + expect( spy1 ).toHaveBeenCalledTimes( 2 ); + expect( spy2 ).toHaveBeenCalledTimes( 1 ); + expect( spy1 ).toHaveLastReturnedWith( 4 ); + expect( spy2 ).toHaveLastReturnedWith( 1 ); + + deepMerge( readOnlyState, { nested: { array: [] } } ); + + expect( spy1 ).toHaveBeenCalledTimes( 2 ); + expect( spy2 ).toHaveBeenCalledTimes( 2 ); + expect( spy1 ).toHaveLastReturnedWith( 4 ); + expect( spy2 ).toHaveLastReturnedWith( undefined ); + } ); + } ); } ); } ); diff --git a/packages/interactivity/src/scopes.ts b/packages/interactivity/src/scopes.ts index 2e78755ec4bbe6..722305f6bee112 100644 --- a/packages/interactivity/src/scopes.ts +++ b/packages/interactivity/src/scopes.ts @@ -12,6 +12,7 @@ import type { Evaluate } from './hooks'; export interface Scope { evaluate: Evaluate; context: object; + serverContext: object; ref: RefObject< HTMLElement >; attributes: createElement.JSX.HTMLAttributes; } @@ -96,3 +97,46 @@ export const getElement = () => { attributes: deepImmutable( attributes ), } ); }; + +/** + * Retrieves the part of the inherited context defined and updated from the + * server. + * + * The object returned is read-only, and includes the context defined in PHP + * with `wp_interactivity_data_wp_context()`, including the corresponding + * inherited properties. When `actions.navigate()` is called, this object is + * updated to reflect the changes in the new visited page, without affecting the + * context returned by `getContext()`. Directives can subscribe to those changes + * to update the context if needed. + * + * @example + * ```js + * store('...', { + * callbacks: { + * updateServerContext() { + * const context = getContext(); + * const serverContext = getServerContext(); + * // Override some property with the new value that came from the server. + * context.overridableProp = serverContext.overridableProp; + * }, + * }, + * }); + * ``` + * + * @param namespace Store namespace. By default, the namespace where the calling + * function exists is used. + * @return The server context content. + */ +export const getServerContext = < T extends object >( + namespace?: string +): T => { + const scope = getScope(); + if ( globalThis.SCRIPT_DEBUG ) { + if ( ! scope ) { + throw Error( + 'Cannot call `getServerContext()` when there is no scope. If you are using an async function, please consider using a generator instead. If you are using some sort of async callbacks, like `setTimeout`, please wrap the callback with `withScope(callback)`.' + ); + } + } + return scope.serverContext[ namespace || getNamespace() ]; +}; diff --git a/packages/interactivity/src/store.ts b/packages/interactivity/src/store.ts index c74764b902e194..b1ad07459c62c0 100644 --- a/packages/interactivity/src/store.ts +++ b/packages/interactivity/src/store.ts @@ -12,6 +12,7 @@ export const stores = new Map(); const rawStores = new Map(); const storeLocks = new Map(); const storeConfigs = new Map(); +const serverStates = new Map(); /** * Get the defined config for the store with the passed namespace. @@ -22,6 +23,39 @@ const storeConfigs = new Map(); export const getConfig = ( namespace?: string ) => storeConfigs.get( namespace || getNamespace() ) || {}; +/** + * Get the part of the state defined and updated from the server. + * + * The object returned is read-only, and includes the state defined in PHP with + * `wp_interactivity_state()`. When using `actions.navigate()`, this object is + * updated to reflect the changes in its properites, without affecting the state + * returned by `store()`. Directives can subscribe to those changes to update + * the state if needed. + * + * @example + * ```js + * const { state } = store('myStore', { + * callbacks: { + * updateServerState() { + * const serverState = getServerState(); + * // Override some property with the new value that came from the server. + * state.overridableProp = serverState.overridableProp; + * }, + * }, + * }); + * ``` + * + * @param namespace Store's namespace from which to retrieve the server state. + * @return The server state for the given namespace. + */ +export const getServerState = ( namespace?: string ) => { + const ns = namespace || getNamespace(); + if ( ! serverStates.has( ns ) ) { + serverStates.set( ns, proxifyState( ns, {}, { readOnly: true } ) ); + } + return serverStates.get( ns ); +}; + interface StoreOptions { /** * Property to block/unblock private store namespaces. @@ -187,6 +221,7 @@ export const populateServerData = ( data?: { Object.entries( data!.state ).forEach( ( [ namespace, state ] ) => { const st = store< any >( namespace, {}, { lock: universalUnlock } ); deepMerge( st.state, state, false ); + deepMerge( getServerState( namespace ), state ); } ); } if ( isPlainObject( data?.config ) ) { diff --git a/packages/interactivity/src/vdom.ts b/packages/interactivity/src/vdom.ts index 98deca656cfa6c..b533a130e4a6f0 100644 --- a/packages/interactivity/src/vdom.ts +++ b/packages/interactivity/src/vdom.ts @@ -7,6 +7,7 @@ import { h, type ComponentChild, type JSX } from 'preact'; */ import { directivePrefix as p } from './constants'; import { warn } from './utils'; +import { type DirectiveEntry } from './hooks'; const ignoreAttr = `data-${ p }-ignore`; const islandAttr = `data-${ p }-interactive`; @@ -139,26 +140,25 @@ export function toVdom( root: Node ): Array< ComponentChild > { } if ( directives.length ) { - props.__directives = directives.reduce( - ( obj, [ name, ns, value ] ) => { - const directiveMatch = directiveParser.exec( name ); - if ( directiveMatch === null ) { - warn( `Found malformed directive name: ${ name }.` ); - return obj; - } - const prefix = directiveMatch[ 1 ] || ''; - const suffix = directiveMatch[ 2 ] || 'default'; - - obj[ prefix ] = obj[ prefix ] || []; - obj[ prefix ].push( { - namespace: ns ?? currentNamespace(), - value, - suffix, - } ); + props.__directives = directives.reduce< + Record< string, Array< DirectiveEntry > > + >( ( obj, [ name, ns, value ] ) => { + const directiveMatch = directiveParser.exec( name ); + if ( directiveMatch === null ) { + warn( `Found malformed directive name: ${ name }.` ); return obj; - }, - {} - ); + } + const prefix = directiveMatch[ 1 ] || ''; + const suffix = directiveMatch[ 2 ] || null; + + obj[ prefix ] = obj[ prefix ] || []; + obj[ prefix ].push( { + namespace: ns ?? currentNamespace()!, + value: value as DirectiveEntry[ 'value' ], + suffix, + } ); + return obj; + }, {} ); } // @ts-expect-error Fixed in upcoming preact release https://github.com/preactjs/preact/pull/4334 diff --git a/packages/interface/CHANGELOG.md b/packages/interface/CHANGELOG.md index 64f8c370bb6ece..64ef4902f4a3ad 100644 --- a/packages/interface/CHANGELOG.md +++ b/packages/interface/CHANGELOG.md @@ -1,6 +1,12 @@ -## Unreleased +## 6.8.6 (2024-10-14) + +### Bug fix + +- `InterfaceSkeleton` no longer supports region navigation and its props `enableRegionNavigation` and `shortcuts` are removed. ([#63611](https://github.com/WordPress/gutenberg/pull/63611)). It’s recommended to add region navigation with the higher-order component `navigateRegions` or the hook `__unstableUseNavigateRegions` from `@wordpress/components`. + +## 6.8.0 (2024-09-19) ## 6.7.0 (2024-09-05) diff --git a/packages/interface/package.json b/packages/interface/package.json index d15d648eaef446..4e580bed0ea271 100644 --- a/packages/interface/package.json +++ b/packages/interface/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/interface", - "version": "6.7.0", + "version": "6.8.10", "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/interface/src/components/complementary-area-toggle/index.js b/packages/interface/src/components/complementary-area-toggle/index.js index b6690b7df5fc5d..2f8d8dd413674b 100644 --- a/packages/interface/src/components/complementary-area-toggle/index.js +++ b/packages/interface/src/components/complementary-area-toggle/index.js @@ -10,6 +10,25 @@ import { useDispatch, useSelect } from '@wordpress/data'; import { store as interfaceStore } from '../../store'; import complementaryAreaContext from '../complementary-area-context'; +/** + * Whether the role supports checked state. + * + * @param {import('react').AriaRole} role Role. + * @return {boolean} Whether the role supports checked state. + * @see https://www.w3.org/TR/wai-aria-1.1/#aria-checked + */ +function roleSupportsCheckedState( role ) { + return [ + 'checkbox', + 'option', + 'radio', + 'switch', + 'menuitemcheckbox', + 'menuitemradio', + 'treeitem', + ].includes( role ); +} + function ComplementaryAreaToggle( { as = Button, scope, @@ -17,6 +36,7 @@ function ComplementaryAreaToggle( { icon, selectedIcon, name, + shortcut, ...props } ) { const ComponentToUse = as; @@ -26,12 +46,18 @@ function ComplementaryAreaToggle( { identifier, [ identifier, scope ] ); + const { enableComplementaryArea, disableComplementaryArea } = useDispatch( interfaceStore ); + return ( { if ( isSelected ) { disableComplementaryArea( scope ); @@ -39,6 +65,7 @@ function ComplementaryAreaToggle( { enableComplementaryArea( scope, identifier ); } } } + shortcut={ shortcut } { ...props } /> ); diff --git a/packages/interface/src/components/complementary-area/index.js b/packages/interface/src/components/complementary-area/index.js index 363a6ee9dea76c..d9fa8e71acb23a 100644 --- a/packages/interface/src/components/complementary-area/index.js +++ b/packages/interface/src/components/complementary-area/index.js @@ -275,6 +275,7 @@ function ComplementaryArea( { showTooltip={ ! showIconLabels } variant={ showIconLabels ? 'tertiary' : undefined } size="compact" + shortcut={ toggleShortcut } /> ) } diff --git a/packages/interface/src/components/interface-skeleton/index.js b/packages/interface/src/components/interface-skeleton/index.js index a1fd20b642206f..2417cc909d1698 100644 --- a/packages/interface/src/components/interface-skeleton/index.js +++ b/packages/interface/src/components/interface-skeleton/index.js @@ -8,13 +8,11 @@ import clsx from 'clsx'; */ import { forwardRef, useEffect } from '@wordpress/element'; import { - __unstableUseNavigateRegions as useNavigateRegions, __unstableMotion as motion, __unstableAnimatePresence as AnimatePresence, } from '@wordpress/components'; import { __, _x } from '@wordpress/i18n'; import { - useMergeRefs, useReducedMotion, useViewportMatch, useResizeObserver, @@ -85,10 +83,6 @@ function InterfaceSkeleton( actions, labels, className, - enableRegionNavigation = true, - // Todo: does this need to be a prop. - // Can we use a dependency to keyboard-shortcuts directly? - shortcuts, }, ref ) { @@ -101,7 +95,6 @@ function InterfaceSkeleton( duration: disableMotion ? 0 : ANIMATION_DURATION, ease: [ 0.6, 0, 0.4, 1 ], }; - const navigateRegionsProps = useNavigateRegions( shortcuts ); useHTMLClass( 'interface-interface-skeleton__html-container' ); const defaultLabels = { @@ -112,7 +105,7 @@ function InterfaceSkeleton( /* translators: accessibility text for the secondary sidebar landmark region. */ secondarySidebar: __( 'Block Library' ), /* translators: accessibility text for the settings landmark region. */ - sidebar: __( 'Settings' ), + sidebar: _x( 'Settings', 'settings landmark area' ), /* translators: accessibility text for the publish landmark region. */ actions: __( 'Publish' ), /* translators: accessibility text for the footer landmark region. */ @@ -123,15 +116,10 @@ function InterfaceSkeleton( return (
diff --git a/packages/interface/src/components/navigable-region/index.js b/packages/interface/src/components/navigable-region/index.js index a4c051185b63ab..50b98d49070d43 100644 --- a/packages/interface/src/components/navigable-region/index.js +++ b/packages/interface/src/components/navigable-region/index.js @@ -1,24 +1,29 @@ +/** + * WordPress dependencies + */ +import { forwardRef } from '@wordpress/element'; + /** * External dependencies */ import clsx from 'clsx'; -export default function NavigableRegion( { - children, - className, - ariaLabel, - as: Tag = 'div', - ...props -} ) { - return ( - - { children } - - ); -} +const NavigableRegion = forwardRef( + ( { children, className, ariaLabel, as: Tag = 'div', ...props }, ref ) => { + return ( + + { children } + + ); + } +); + +NavigableRegion.displayName = 'NavigableRegion'; +export default NavigableRegion; diff --git a/packages/is-shallow-equal/CHANGELOG.md b/packages/is-shallow-equal/CHANGELOG.md index 81f2c4b164a3ab..ef424aaeebf220 100644 --- a/packages/is-shallow-equal/CHANGELOG.md +++ b/packages/is-shallow-equal/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 5.8.0 (2024-09-19) + ## 5.7.0 (2024-09-05) ## 5.6.0 (2024-08-21) diff --git a/packages/is-shallow-equal/package.json b/packages/is-shallow-equal/package.json index 553c3182bb5668..cc8093ae14d550 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": "5.7.0", + "version": "5.8.1", "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 7f7f80b815a850..2ed1dc24299f45 100644 --- a/packages/jest-console/CHANGELOG.md +++ b/packages/jest-console/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 8.8.0 (2024-09-19) + ## 8.7.0 (2024-09-05) ## 8.6.0 (2024-08-21) diff --git a/packages/jest-console/package.json b/packages/jest-console/package.json index 4381a0f287235f..1cbb8d68dc2232 100644 --- a/packages/jest-console/package.json +++ b/packages/jest-console/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/jest-console", - "version": "8.7.0", + "version": "8.8.1", "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 e48d25dc1e6424..a3ecded683cd17 100644 --- a/packages/jest-preset-default/CHANGELOG.md +++ b/packages/jest-preset-default/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 12.8.0 (2024-09-19) + ## 12.7.0 (2024-09-05) ## 12.6.0 (2024-08-21) diff --git a/packages/jest-preset-default/package.json b/packages/jest-preset-default/package.json index 27638c711d4d1e..af599dfce09493 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": "12.7.0", + "version": "12.8.1", "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 572b4aa137a141..3e61ea09412d63 100644 --- a/packages/jest-puppeteer-axe/CHANGELOG.md +++ b/packages/jest-puppeteer-axe/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 7.8.0 (2024-09-19) + ## 7.7.0 (2024-09-05) ## 7.6.0 (2024-08-21) diff --git a/packages/jest-puppeteer-axe/package.json b/packages/jest-puppeteer-axe/package.json index b8ccbfcd36beff..7f0ff64ed7744b 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": "7.7.0", + "version": "7.8.1", "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 fe3506ea4a5f19..356e850692af4d 100644 --- a/packages/keyboard-shortcuts/CHANGELOG.md +++ b/packages/keyboard-shortcuts/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 5.8.0 (2024-09-19) + ## 5.7.0 (2024-09-05) ## 5.6.0 (2024-08-21) diff --git a/packages/keyboard-shortcuts/package.json b/packages/keyboard-shortcuts/package.json index dd560424b66ff7..ee0ac7391f5754 100644 --- a/packages/keyboard-shortcuts/package.json +++ b/packages/keyboard-shortcuts/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/keyboard-shortcuts", - "version": "5.7.0", + "version": "5.8.3", "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 51050728f2f5a3..a578adb34bd3d3 100644 --- a/packages/keycodes/CHANGELOG.md +++ b/packages/keycodes/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 4.8.0 (2024-09-19) + ## 4.7.0 (2024-09-05) ## 4.6.0 (2024-08-21) diff --git a/packages/keycodes/package.json b/packages/keycodes/package.json index 5d8ec0545d7e3f..1081c75644452f 100644 --- a/packages/keycodes/package.json +++ b/packages/keycodes/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/keycodes", - "version": "4.7.0", + "version": "4.8.2", "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 daea6da3404d3f..6853a036fdaa50 100644 --- a/packages/lazy-import/CHANGELOG.md +++ b/packages/lazy-import/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 2.8.0 (2024-09-19) + ## 2.7.0 (2024-09-05) ## 2.6.0 (2024-08-21) diff --git a/packages/lazy-import/package.json b/packages/lazy-import/package.json index 62aab79635e9c4..3fbc99c1e9dbad 100644 --- a/packages/lazy-import/package.json +++ b/packages/lazy-import/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/lazy-import", - "version": "2.7.0", + "version": "2.8.1", "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 9f62523fd94e11..852f30a2999175 100644 --- a/packages/list-reusable-blocks/CHANGELOG.md +++ b/packages/list-reusable-blocks/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 5.8.0 (2024-09-19) + ## 5.7.0 (2024-09-05) ## 5.6.0 (2024-08-21) diff --git a/packages/list-reusable-blocks/package.json b/packages/list-reusable-blocks/package.json index 839532327faa24..7661b78ae44979 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": "5.7.0", + "version": "5.8.10", "description": "Adding Export/Import support to the reusable blocks listing.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/list-reusable-blocks/src/components/import-dropdown/index.js b/packages/list-reusable-blocks/src/components/import-dropdown/index.js index d20ba9fcf10999..fdad08f80d213c 100644 --- a/packages/list-reusable-blocks/src/components/import-dropdown/index.js +++ b/packages/list-reusable-blocks/src/components/import-dropdown/index.js @@ -17,8 +17,8 @@ function ImportDropdown( { onUpload } ) { contentClassName="list-reusable-blocks-import-dropdown__content" renderToggle={ ( { isOpen, onToggle } ) => (

); diff --git a/test/e2e/specs/editor/blocks/cover.spec.js b/test/e2e/specs/editor/blocks/cover.spec.js index cf3005c9eaa18f..a9a93caa4f4341 100644 --- a/test/e2e/specs/editor/blocks/cover.spec.js +++ b/test/e2e/specs/editor/blocks/cover.spec.js @@ -226,6 +226,77 @@ test.describe( 'Cover', () => { await expect( overlay ).toHaveCSS( 'background-color', 'rgb(0, 0, 0)' ); await expect( overlay ).toHaveCSS( 'opacity', '0.5' ); } ); + + test( 'other cover blocks are not over the navigation block when the menu is open', async ( { + editor, + page, + } ) => { + // Insert a Cover block + await editor.insertBlock( { name: 'core/cover' } ); + const coverBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Cover', + } ); + + // Choose a color swatch to transform the placeholder block into + // a functioning block. + await coverBlock + .getByRole( 'option', { + name: 'Color: Black', + } ) + .click(); + + // Insert a Navigation block inside the Cover block + await editor.selectBlocks( coverBlock ); + await coverBlock.getByRole( 'button', { name: 'Add block' } ).click(); + await page.keyboard.type( 'Navigation' ); + const blockResults = page.getByRole( 'listbox', { + name: 'Blocks', + } ); + const blockResultOptions = blockResults.getByRole( 'option' ); + await blockResultOptions.nth( 0 ).click(); + + // Insert a second Cover block. + await editor.insertBlock( { name: 'core/cover' } ); + const secondCoverBlock = editor.canvas + .getByRole( 'document', { + name: 'Block: Cover', + } ) + .last(); + + // Choose a color swatch to transform the placeholder block into + // a functioning block. + await secondCoverBlock + .getByRole( 'option', { + name: 'Color: Black', + } ) + .click(); + + // Set the viewport to a small screen and open menu. + await page.setViewportSize( { width: 375, height: 1000 } ); + const navigationBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Navigation', + } ); + await editor.selectBlocks( navigationBlock ); + await editor.canvas + .getByRole( 'button', { name: 'Open menu' } ) + .click(); + + // Check if inner container of the second cover is clickable. + const secondInnerContainer = secondCoverBlock.locator( + '.wp-block-cover__inner-container' + ); + let isClickable; + try { + isClickable = await secondInnerContainer.click( { + trial: true, + timeout: 1000, // This test will always take 1 second to run. + } ); + } catch ( error ) { + isClickable = false; + } + + expect( isClickable ).toBe( false ); + } ); } ); class CoverBlockUtils { diff --git a/test/e2e/specs/editor/various/__snapshots__/Copy-cut-paste-should-paste-link-to-formatted-text-1-chromium.txt b/test/e2e/specs/editor/various/__snapshots__/Copy-cut-paste-should-paste-link-to-formatted-text-1-chromium.txt new file mode 100644 index 00000000000000..73ed4090549910 --- /dev/null +++ b/test/e2e/specs/editor/various/__snapshots__/Copy-cut-paste-should-paste-link-to-formatted-text-1-chromium.txt @@ -0,0 +1,3 @@ + +

test

+ \ No newline at end of file diff --git a/test/e2e/specs/editor/various/block-bindings.spec.js b/test/e2e/specs/editor/various/block-bindings.spec.js deleted file mode 100644 index c556c469698ebd..00000000000000 --- a/test/e2e/specs/editor/various/block-bindings.spec.js +++ /dev/null @@ -1,2415 +0,0 @@ -/** - * External dependencies - */ -const path = require( 'path' ); -/** - * WordPress dependencies - */ -const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); - -test.describe( 'Block bindings', () => { - let imagePlaceholderSrc; - let imageCustomFieldSrc; - test.beforeAll( async ( { requestUtils } ) => { - await requestUtils.activateTheme( 'emptytheme' ); - await requestUtils.activatePlugin( 'gutenberg-test-block-bindings' ); - await requestUtils.deleteAllMedia(); - const placeholderMedia = await requestUtils.uploadMedia( - path.join( './test/e2e/assets', '10x10_e2e_test_image_z9T8jK.png' ) - ); - imagePlaceholderSrc = placeholderMedia.source_url; - } ); - - test.afterEach( async ( { requestUtils } ) => { - await requestUtils.deleteAllPosts(); - } ); - - test.afterAll( async ( { requestUtils } ) => { - await requestUtils.deleteAllMedia(); - await requestUtils.activateTheme( 'twentytwentyone' ); - await requestUtils.deactivatePlugin( 'gutenberg-test-block-bindings' ); - } ); - - test.describe( 'Template context', () => { - test.beforeEach( async ( { admin, editor } ) => { - await admin.visitSiteEditor( { - postId: 'emptytheme//index', - postType: 'wp_template', - canvas: 'edit', - } ); - await editor.openDocumentSettingsSidebar(); - } ); - - test.describe( 'Paragraph', () => { - test( 'should show the key of the custom field in post meta', async ( { - editor, - } ) => { - await editor.insertBlock( { - name: 'core/paragraph', - attributes: { - content: 'paragraph default content', - metadata: { - bindings: { - content: { - source: 'core/post-meta', - args: { key: 'text_custom_field' }, - }, - }, - }, - }, - } ); - const paragraphBlock = editor.canvas.getByRole( 'document', { - name: 'Block: Paragraph', - } ); - await expect( paragraphBlock ).toHaveText( - 'text_custom_field' - ); - } ); - - test( 'should show the key of the custom field in server sources with key', async ( { - editor, - } ) => { - await editor.insertBlock( { - name: 'core/paragraph', - attributes: { - content: 'paragraph default content', - metadata: { - bindings: { - content: { - source: 'core/server-source', - args: { key: 'text_custom_field' }, - }, - }, - }, - }, - } ); - const paragraphBlock = editor.canvas.getByRole( 'document', { - name: 'Block: Paragraph', - } ); - await expect( paragraphBlock ).toHaveText( - 'text_custom_field' - ); - } ); - - test( 'should show the source label in server sources without key', async ( { - editor, - } ) => { - await editor.insertBlock( { - name: 'core/paragraph', - attributes: { - content: 'paragraph default content', - metadata: { - bindings: { - content: { - source: 'core/server-source', - }, - }, - }, - }, - } ); - const paragraphBlock = editor.canvas.getByRole( 'document', { - name: 'Block: Paragraph', - } ); - await expect( paragraphBlock ).toHaveText( 'Server Source' ); - } ); - - test( 'should lock the appropriate controls with a registered source', async ( { - editor, - page, - } ) => { - await editor.insertBlock( { - name: 'core/paragraph', - attributes: { - content: 'paragraph default content', - metadata: { - bindings: { - content: { - source: 'core/post-meta', - args: { key: 'text_custom_field' }, - }, - }, - }, - }, - } ); - const paragraphBlock = editor.canvas.getByRole( 'document', { - name: 'Block: Paragraph', - } ); - await paragraphBlock.click(); - - // Alignment controls exist. - await expect( - page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { name: 'Align text' } ) - ).toBeVisible(); - - // Format controls don't exist. - await expect( - page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { - name: 'Bold', - } ) - ).toBeHidden(); - - // Paragraph is not editable. - await expect( paragraphBlock ).toHaveAttribute( - 'contenteditable', - 'false' - ); - } ); - - test( 'should lock the appropriate controls when source is not defined', async ( { - editor, - page, - } ) => { - await editor.insertBlock( { - name: 'core/paragraph', - attributes: { - content: 'paragraph default content', - metadata: { - bindings: { - content: { - source: 'plugin/undefined-source', - args: { key: 'text_custom_field' }, - }, - }, - }, - }, - } ); - const paragraphBlock = editor.canvas.getByRole( 'document', { - name: 'Block: Paragraph', - } ); - await paragraphBlock.click(); - - // Alignment controls exist. - await expect( - page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { name: 'Align text' } ) - ).toBeVisible(); - - // Format controls don't exist. - await expect( - page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { - name: 'Bold', - } ) - ).toBeHidden(); - - // Paragraph is not editable. - await expect( paragraphBlock ).toHaveAttribute( - 'contenteditable', - 'false' - ); - } ); - } ); - - test.describe( 'Heading', () => { - test( 'should show the key of the custom field', async ( { - editor, - } ) => { - await editor.insertBlock( { - name: 'core/heading', - attributes: { - content: 'heading default content', - metadata: { - bindings: { - content: { - source: 'core/post-meta', - args: { key: 'text_custom_field' }, - }, - }, - }, - }, - } ); - const headingBlock = editor.canvas.getByRole( 'document', { - name: 'Block: Heading', - } ); - await expect( headingBlock ).toHaveText( 'text_custom_field' ); - } ); - - test( 'should lock the appropriate controls with a registered source', async ( { - editor, - page, - } ) => { - await editor.insertBlock( { - name: 'core/heading', - attributes: { - content: 'heading default content', - metadata: { - bindings: { - content: { - source: 'core/post-meta', - args: { key: 'text_custom_field' }, - }, - }, - }, - }, - } ); - const headingBlock = editor.canvas.getByRole( 'document', { - name: 'Block: Heading', - } ); - await headingBlock.click(); - - // Alignment controls exist. - await expect( - page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { name: 'Align text' } ) - ).toBeVisible(); - - // Format controls don't exist. - await expect( - page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { - name: 'Bold', - } ) - ).toBeHidden(); - - // Heading is not editable. - await expect( headingBlock ).toHaveAttribute( - 'contenteditable', - 'false' - ); - } ); - - test( 'should lock the appropriate controls when source is not defined', async ( { - editor, - page, - } ) => { - await editor.insertBlock( { - name: 'core/heading', - attributes: { - content: 'heading default content', - metadata: { - bindings: { - content: { - source: 'plugin/undefined-source', - args: { key: 'text_custom_field' }, - }, - }, - }, - }, - } ); - const headingBlock = editor.canvas.getByRole( 'document', { - name: 'Block: Heading', - } ); - await headingBlock.click(); - - // Alignment controls exist. - await expect( - page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { name: 'Align text' } ) - ).toBeVisible(); - - // Format controls don't exist. - await expect( - page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { - name: 'Bold', - } ) - ).toBeHidden(); - - // Heading is not editable. - await expect( headingBlock ).toHaveAttribute( - 'contenteditable', - 'false' - ); - } ); - } ); - - test.describe( 'Button', () => { - test( 'should show the key of the custom field when text is bound', async ( { - editor, - } ) => { - await editor.insertBlock( { - name: 'core/buttons', - innerBlocks: [ - { - name: 'core/button', - attributes: { - text: 'button default text', - url: '#default-url', - metadata: { - bindings: { - text: { - source: 'core/post-meta', - args: { key: 'text_custom_field' }, - }, - }, - }, - }, - }, - ], - } ); - const buttonBlock = editor.canvas.getByRole( 'document', { - name: 'Block: Button', - exact: true, - } ); - await expect( buttonBlock ).toHaveText( 'text_custom_field' ); - } ); - - test( 'should lock text controls when text is bound to a registered source', async ( { - editor, - page, - } ) => { - await editor.insertBlock( { - name: 'core/buttons', - innerBlocks: [ - { - name: 'core/button', - attributes: { - text: 'button default text', - url: '#default-url', - metadata: { - bindings: { - text: { - source: 'core/post-meta', - args: { key: 'text_custom_field' }, - }, - }, - }, - }, - }, - ], - } ); - const buttonBlock = editor.canvas - .getByRole( 'document', { - name: 'Block: Button', - exact: true, - } ) - .getByRole( 'textbox' ); - await buttonBlock.click(); - - // Alignment controls exist. - await expect( - page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { name: 'Align text' } ) - ).toBeVisible(); - - // Format controls don't exist. - await expect( - page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { - name: 'Bold', - } ) - ).toBeHidden(); - - // Button is not editable. - await expect( buttonBlock ).toHaveAttribute( - 'contenteditable', - 'false' - ); - - // Link controls exist. - await expect( - page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { name: 'Unlink' } ) - ).toBeVisible(); - } ); - - test( 'should lock text controls when text is bound to an undefined source', async ( { - editor, - page, - } ) => { - await editor.insertBlock( { - name: 'core/buttons', - innerBlocks: [ - { - name: 'core/button', - attributes: { - text: 'button default text', - url: '#default-url', - metadata: { - bindings: { - text: { - source: 'plugin/undefined-source', - args: { key: 'text_custom_field' }, - }, - }, - }, - }, - }, - ], - } ); - const buttonBlock = editor.canvas - .getByRole( 'document', { - name: 'Block: Button', - exact: true, - } ) - .getByRole( 'textbox' ); - await buttonBlock.click(); - - // Alignment controls exist. - await expect( - page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { name: 'Align text' } ) - ).toBeVisible(); - - // Format controls don't exist. - await expect( - page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { - name: 'Bold', - } ) - ).toBeHidden(); - - // Button is not editable. - await expect( buttonBlock ).toHaveAttribute( - 'contenteditable', - 'false' - ); - - // Link controls exist. - await expect( - page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { name: 'Unlink' } ) - ).toBeVisible(); - } ); - - test( 'should lock url controls when url is bound to a registered source', async ( { - editor, - page, - } ) => { - await editor.insertBlock( { - name: 'core/buttons', - innerBlocks: [ - { - name: 'core/button', - attributes: { - text: 'button default text', - url: '#default-url', - metadata: { - bindings: { - url: { - source: 'core/post-meta', - args: { key: 'url_custom_field' }, - }, - }, - }, - }, - }, - ], - } ); - const buttonBlock = editor.canvas - .getByRole( 'document', { - name: 'Block: Button', - exact: true, - } ) - .getByRole( 'textbox' ); - await buttonBlock.click(); - - // Format controls exist. - await expect( - page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { - name: 'Bold', - } ) - ).toBeVisible(); - - // Button is editable. - await expect( buttonBlock ).toHaveAttribute( - 'contenteditable', - 'true' - ); - - // Link controls don't exist. - await expect( - page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { name: 'Link' } ) - ).toBeHidden(); - await expect( - page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { name: 'Unlink' } ) - ).toBeHidden(); - } ); - - test( 'should lock url controls when url is bound to an undefined source', async ( { - editor, - page, - } ) => { - await editor.insertBlock( { - name: 'core/buttons', - innerBlocks: [ - { - name: 'core/button', - attributes: { - text: 'button default text', - url: '#default-url', - metadata: { - bindings: { - url: { - source: 'plugin/undefined-source', - args: { key: 'url_custom_field' }, - }, - }, - }, - }, - }, - ], - } ); - const buttonBlock = editor.canvas - .getByRole( 'document', { - name: 'Block: Button', - exact: true, - } ) - .getByRole( 'textbox' ); - await buttonBlock.click(); - - // Format controls exist. - await expect( - page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { - name: 'Bold', - } ) - ).toBeVisible(); - - // Button is editable. - await expect( buttonBlock ).toHaveAttribute( - 'contenteditable', - 'true' - ); - - // Link controls don't exist. - await expect( - page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { name: 'Link' } ) - ).toBeHidden(); - await expect( - page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { name: 'Unlink' } ) - ).toBeHidden(); - } ); - - test( 'should lock url and text controls when both are bound', async ( { - editor, - page, - } ) => { - await editor.insertBlock( { - name: 'core/buttons', - innerBlocks: [ - { - name: 'core/button', - attributes: { - text: 'button default text', - url: '#default-url', - metadata: { - bindings: { - text: { - source: 'core/post-meta', - args: { key: 'text_custom_field' }, - }, - url: { - source: 'core/post-meta', - args: { key: 'url_custom_field' }, - }, - }, - }, - }, - }, - ], - } ); - const buttonBlock = editor.canvas - .getByRole( 'document', { - name: 'Block: Button', - exact: true, - } ) - .getByRole( 'textbox' ); - await buttonBlock.click(); - - // Alignment controls are visible. - await expect( - page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { name: 'Align text' } ) - ).toBeVisible(); - - // Format controls don't exist. - await expect( - page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { - name: 'Bold', - } ) - ).toBeHidden(); - - // Button is not editable. - await expect( buttonBlock ).toHaveAttribute( - 'contenteditable', - 'false' - ); - - // Link controls don't exist. - await expect( - page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { name: 'Link' } ) - ).toBeHidden(); - await expect( - page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { name: 'Unlink' } ) - ).toBeHidden(); - } ); - } ); - - test.describe( 'Image', () => { - test( 'should show the upload form when url is not bound', async ( { - editor, - } ) => { - await editor.insertBlock( { name: 'core/image' } ); - const imageBlock = editor.canvas.getByRole( 'document', { - name: 'Block: Image', - } ); - await imageBlock.click(); - await expect( - imageBlock.getByRole( 'button', { name: 'Upload' } ) - ).toBeVisible(); - } ); - - test( 'should NOT show the upload form when url is bound to a registered source', async ( { - editor, - } ) => { - await editor.insertBlock( { - name: 'core/image', - attributes: { - url: imagePlaceholderSrc, - alt: 'default alt value', - title: 'default title value', - metadata: { - bindings: { - url: { - source: 'core/post-meta', - args: { key: 'url_custom_field' }, - }, - }, - }, - }, - } ); - const imageBlock = editor.canvas.getByRole( 'document', { - name: 'Block: Image', - } ); - await imageBlock.click(); - await expect( - imageBlock.getByRole( 'button', { name: 'Upload' } ) - ).toBeHidden(); - } ); - - test( 'should NOT show the upload form when url is bound to an undefined source', async ( { - editor, - } ) => { - await editor.insertBlock( { - name: 'core/image', - attributes: { - url: imagePlaceholderSrc, - alt: 'default alt value', - title: 'default title value', - metadata: { - bindings: { - url: { - source: 'plugin/undefined-source', - args: { key: 'url_custom_field' }, - }, - }, - }, - }, - } ); - const imageBlock = editor.canvas.getByRole( 'document', { - name: 'Block: Image', - } ); - await imageBlock.click(); - await expect( - imageBlock.getByRole( 'button', { name: 'Upload' } ) - ).toBeHidden(); - } ); - - test( 'should lock url controls when url is bound to a registered source', async ( { - editor, - page, - } ) => { - await editor.insertBlock( { - name: 'core/image', - attributes: { - url: imagePlaceholderSrc, - alt: 'default alt value', - title: 'default title value', - metadata: { - bindings: { - url: { - source: 'core/post-meta', - args: { key: 'url_custom_field' }, - }, - }, - }, - }, - } ); - const imageBlock = editor.canvas.getByRole( 'document', { - name: 'Block: Image', - } ); - await imageBlock.click(); - - // Replace controls don't exist. - await expect( - page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { - name: 'Replace', - } ) - ).toBeHidden(); - - // Image placeholder doesn't show the upload button. - await expect( - imageBlock.getByRole( 'button', { name: 'Upload' } ) - ).toBeHidden(); - - // Alt textarea is enabled and with the original value. - await expect( - page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Alternative text' ) - ).toBeEnabled(); - const altValue = await page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Alternative text' ) - .inputValue(); - expect( altValue ).toBe( 'default alt value' ); - - // Title input is enabled and with the original value. - await page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByRole( 'button', { name: 'Advanced' } ) - .click(); - - await expect( - page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Title attribute' ) - ).toBeEnabled(); - const titleValue = await page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Title attribute' ) - .inputValue(); - expect( titleValue ).toBe( 'default title value' ); - } ); - - test( 'should lock url controls when url is bound to an undefined source', async ( { - editor, - page, - } ) => { - await editor.insertBlock( { - name: 'core/image', - attributes: { - url: imagePlaceholderSrc, - alt: 'default alt value', - title: 'default title value', - metadata: { - bindings: { - url: { - source: 'plugin/undefined-source', - args: { key: 'url_custom_field' }, - }, - }, - }, - }, - } ); - const imageBlock = editor.canvas.getByRole( 'document', { - name: 'Block: Image', - } ); - await imageBlock.click(); - - // Replace controls don't exist. - await expect( - page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { - name: 'Replace', - } ) - ).toBeHidden(); - - // Image placeholder doesn't show the upload button. - await expect( - imageBlock.getByRole( 'button', { name: 'Upload' } ) - ).toBeHidden(); - - // Alt textarea is enabled and with the original value. - await expect( - page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Alternative text' ) - ).toBeEnabled(); - const altValue = await page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Alternative text' ) - .inputValue(); - expect( altValue ).toBe( 'default alt value' ); - - // Title input is enabled and with the original value. - await page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByRole( 'button', { name: 'Advanced' } ) - .click(); - - await expect( - page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Title attribute' ) - ).toBeEnabled(); - const titleValue = await page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Title attribute' ) - .inputValue(); - expect( titleValue ).toBe( 'default title value' ); - } ); - - test( 'should disable alt textarea when alt is bound to a registered source', async ( { - editor, - page, - } ) => { - await editor.insertBlock( { - name: 'core/image', - attributes: { - url: imagePlaceholderSrc, - alt: 'default alt value', - title: 'default title value', - metadata: { - bindings: { - alt: { - source: 'core/post-meta', - args: { key: 'text_custom_field' }, - }, - }, - }, - }, - } ); - const imageBlock = editor.canvas.getByRole( 'document', { - name: 'Block: Image', - } ); - await imageBlock.click(); - - // Replace controls exist. - await expect( - page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { - name: 'Replace', - } ) - ).toBeVisible(); - - // Alt textarea is disabled and with the custom field value. - await expect( - page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Alternative text' ) - ).toHaveAttribute( 'readonly' ); - const altValue = await page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Alternative text' ) - .inputValue(); - expect( altValue ).toBe( 'text_custom_field' ); - - // Title input is enabled and with the original value. - await page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByRole( 'button', { name: 'Advanced' } ) - .click(); - await expect( - page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Title attribute' ) - ).toBeEnabled(); - const titleValue = await page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Title attribute' ) - .inputValue(); - expect( titleValue ).toBe( 'default title value' ); - } ); - - test( 'should disable alt textarea when alt is bound to an undefined source', async ( { - editor, - page, - } ) => { - await editor.insertBlock( { - name: 'core/image', - attributes: { - url: imagePlaceholderSrc, - alt: 'default alt value', - title: 'default title value', - metadata: { - bindings: { - alt: { - source: 'plguin/undefined-source', - args: { key: 'text_custom_field' }, - }, - }, - }, - }, - } ); - const imageBlock = editor.canvas.getByRole( 'document', { - name: 'Block: Image', - } ); - await imageBlock.click(); - - // Replace controls exist. - await expect( - page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { - name: 'Replace', - } ) - ).toBeVisible(); - - // Alt textarea is disabled and with the custom field value. - await expect( - page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Alternative text' ) - ).toHaveAttribute( 'readonly' ); - const altValue = await page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Alternative text' ) - .inputValue(); - expect( altValue ).toBe( 'default alt value' ); - - // Title input is enabled and with the original value. - await page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByRole( 'button', { name: 'Advanced' } ) - .click(); - await expect( - page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Title attribute' ) - ).toBeEnabled(); - const titleValue = await page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Title attribute' ) - .inputValue(); - expect( titleValue ).toBe( 'default title value' ); - } ); - - test( 'should disable title input when title is bound to a registered source', async ( { - editor, - page, - } ) => { - await editor.insertBlock( { - name: 'core/image', - attributes: { - url: imagePlaceholderSrc, - alt: 'default alt value', - title: 'default title value', - metadata: { - bindings: { - title: { - source: 'core/post-meta', - args: { key: 'text_custom_field' }, - }, - }, - }, - }, - } ); - const imageBlock = editor.canvas.getByRole( 'document', { - name: 'Block: Image', - } ); - await imageBlock.click(); - - // Replace controls exist. - await expect( - page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { - name: 'Replace', - } ) - ).toBeVisible(); - - // Alt textarea is enabled and with the original value. - await expect( - page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Alternative text' ) - ).toBeEnabled(); - const altValue = await page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Alternative text' ) - .inputValue(); - expect( altValue ).toBe( 'default alt value' ); - - // Title input is disabled and with the custom field value. - await page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByRole( 'button', { name: 'Advanced' } ) - .click(); - await expect( - page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Title attribute' ) - ).toHaveAttribute( 'readonly' ); - const titleValue = await page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Title attribute' ) - .inputValue(); - expect( titleValue ).toBe( 'text_custom_field' ); - } ); - - test( 'should disable title input when title is bound to an undefined source', async ( { - editor, - page, - } ) => { - await editor.insertBlock( { - name: 'core/image', - attributes: { - url: imagePlaceholderSrc, - alt: 'default alt value', - title: 'default title value', - metadata: { - bindings: { - title: { - source: 'plugin/undefined-source', - args: { key: 'text_custom_field' }, - }, - }, - }, - }, - } ); - const imageBlock = editor.canvas.getByRole( 'document', { - name: 'Block: Image', - } ); - await imageBlock.click(); - - // Replace controls exist. - await expect( - page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { - name: 'Replace', - } ) - ).toBeVisible(); - - // Alt textarea is enabled and with the original value. - await expect( - page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Alternative text' ) - ).toBeEnabled(); - const altValue = await page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Alternative text' ) - .inputValue(); - expect( altValue ).toBe( 'default alt value' ); - - // Title input is disabled and with the custom field value. - await page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByRole( 'button', { name: 'Advanced' } ) - .click(); - await expect( - page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Title attribute' ) - ).toHaveAttribute( 'readonly' ); - const titleValue = await page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Title attribute' ) - .inputValue(); - expect( titleValue ).toBe( 'default title value' ); - } ); - - test( 'Multiple bindings should lock the appropriate controls', async ( { - editor, - page, - } ) => { - await editor.insertBlock( { - name: 'core/image', - attributes: { - url: imagePlaceholderSrc, - alt: 'default alt value', - title: 'default title value', - metadata: { - bindings: { - url: { - source: 'core/post-meta', - args: { key: 'url_custom_field' }, - }, - alt: { - source: 'core/post-meta', - args: { key: 'text_custom_field' }, - }, - }, - }, - }, - } ); - const imageBlock = editor.canvas.getByRole( 'document', { - name: 'Block: Image', - } ); - await imageBlock.click(); - - // Replace controls don't exist. - await expect( - page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { - name: 'Replace', - } ) - ).toBeHidden(); - - // Image placeholder doesn't show the upload button. - await expect( - imageBlock.getByRole( 'button', { name: 'Upload' } ) - ).toBeHidden(); - - // Alt textarea is disabled and with the custom field value. - await expect( - page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Alternative text' ) - ).toHaveAttribute( 'readonly' ); - const altValue = await page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Alternative text' ) - .inputValue(); - expect( altValue ).toBe( 'text_custom_field' ); - - // Title input is enabled and with the original value. - await page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByRole( 'button', { name: 'Advanced' } ) - .click(); - await expect( - page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Title attribute' ) - ).toBeEnabled(); - const titleValue = await page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Title attribute' ) - .inputValue(); - expect( titleValue ).toBe( 'default title value' ); - } ); - } ); - } ); - - test.describe( 'Post/page context', () => { - test.beforeEach( async ( { admin } ) => { - await admin.createNewPost( { title: 'Test bindings' } ); - } ); - test.describe( 'Paragraph', () => { - test( 'should show the value of the custom field when exists', async ( { - editor, - } ) => { - await editor.insertBlock( { - name: 'core/paragraph', - attributes: { - anchor: 'paragraph-binding', - content: 'paragraph default content', - metadata: { - bindings: { - content: { - source: 'core/post-meta', - args: { key: 'text_custom_field' }, - }, - }, - }, - }, - } ); - const paragraphBlock = editor.canvas.getByRole( 'document', { - name: 'Block: Paragraph', - } ); - await expect( paragraphBlock ).toHaveText( - 'Value of the text custom field' - ); - - // Check the frontend shows the value of the custom field. - const previewPage = await editor.openPreviewPage(); - await expect( - previewPage.locator( '#paragraph-binding' ) - ).toHaveText( 'Value of the text custom field' ); - } ); - - test( "should show the value of the key when custom field doesn't exist", async ( { - editor, - } ) => { - await editor.insertBlock( { - name: 'core/paragraph', - attributes: { - anchor: 'paragraph-binding', - content: 'fallback value', - metadata: { - bindings: { - content: { - source: 'core/post-meta', - args: { key: 'non_existing_custom_field' }, - }, - }, - }, - }, - } ); - const paragraphBlock = editor.canvas.getByRole( 'document', { - name: 'Block: Paragraph', - } ); - await expect( paragraphBlock ).toHaveText( - 'non_existing_custom_field' - ); - // Paragraph is not editable. - await expect( paragraphBlock ).toHaveAttribute( - 'contenteditable', - 'false' - ); - - // Check the frontend doesn't show the content. - const previewPage = await editor.openPreviewPage(); - await expect( - previewPage.locator( '#paragraph-binding' ) - ).toHaveText( 'fallback value' ); - } ); - - test( 'should show the prompt placeholder in field with empty value', async ( { - editor, - } ) => { - await editor.insertBlock( { - name: 'core/paragraph', - attributes: { - content: 'paragraph default content', - metadata: { - bindings: { - content: { - source: 'core/post-meta', - args: { key: 'empty_field' }, - }, - }, - }, - }, - } ); - - const paragraphBlock = editor.canvas.getByRole( 'document', { - // Aria-label is changed for empty paragraphs. - name: 'Add empty_field', - } ); - - await expect( paragraphBlock ).toBeEmpty(); - - const placeholder = paragraphBlock.locator( 'span' ); - await expect( placeholder ).toHaveAttribute( - 'data-rich-text-placeholder', - 'Add empty_field' - ); - } ); - - test( 'should not show the value of a protected meta field', async ( { - editor, - } ) => { - await editor.insertBlock( { - name: 'core/paragraph', - attributes: { - anchor: 'paragraph-binding', - content: 'fallback value', - metadata: { - bindings: { - content: { - source: 'core/post-meta', - args: { key: '_protected_field' }, - }, - }, - }, - }, - } ); - const paragraphBlock = editor.canvas.getByRole( 'document', { - name: 'Block: Paragraph', - } ); - await expect( paragraphBlock ).toHaveText( '_protected_field' ); - // Check the frontend doesn't show the content. - const previewPage = await editor.openPreviewPage(); - await expect( - previewPage.locator( '#paragraph-binding' ) - ).toHaveText( 'fallback value' ); - } ); - - test( 'should not show the value of a meta field with `show_in_rest` false', async ( { - editor, - } ) => { - await editor.insertBlock( { - name: 'core/paragraph', - attributes: { - anchor: 'paragraph-binding', - content: 'fallback value', - metadata: { - bindings: { - content: { - source: 'core/post-meta', - args: { key: 'show_in_rest_false_field' }, - }, - }, - }, - }, - } ); - const paragraphBlock = editor.canvas.getByRole( 'document', { - name: 'Block: Paragraph', - } ); - await expect( paragraphBlock ).toHaveText( - 'show_in_rest_false_field' - ); - // Check the frontend doesn't show the content. - const previewPage = await editor.openPreviewPage(); - await expect( - previewPage.locator( '#paragraph-binding' ) - ).toHaveText( 'fallback value' ); - } ); - - test( 'should add empty paragraph block when pressing enter', async ( { - editor, - page, - } ) => { - await editor.insertBlock( { - name: 'core/paragraph', - attributes: { - content: 'paragraph default content', - metadata: { - bindings: { - content: { - source: 'core/post-meta', - args: { key: 'text_custom_field' }, - }, - }, - }, - }, - } ); - // Select the paragraph and press Enter at the end of it. - const paragraph = editor.canvas.getByRole( 'document', { - name: 'Block: Paragraph', - } ); - await editor.selectBlocks( paragraph ); - await page.keyboard.press( 'End' ); - await page.keyboard.press( 'Enter' ); - const [ initialParagraph, newEmptyParagraph ] = - await editor.canvas - .locator( '[data-type="core/paragraph"]' ) - .all(); - await expect( initialParagraph ).toHaveText( - 'Value of the text custom field' - ); - await expect( newEmptyParagraph ).toHaveText( '' ); - await expect( newEmptyParagraph ).toBeEditable(); - } ); - - test( 'should NOT be possible to edit the value of the custom field when it is protected', async ( { - editor, - } ) => { - await editor.insertBlock( { - name: 'core/paragraph', - attributes: { - anchor: 'protected-field-binding', - content: 'fallback value', - metadata: { - bindings: { - content: { - source: 'core/post-meta', - args: { key: '_protected_field' }, - }, - }, - }, - }, - } ); - - const protectedFieldBlock = editor.canvas.getByRole( - 'document', - { - name: 'Block: Paragraph', - } - ); - - await expect( protectedFieldBlock ).toHaveAttribute( - 'contenteditable', - 'false' - ); - } ); - - test( 'should NOT be possible to edit the value of the custom field when it is not shown in the REST API', async ( { - editor, - } ) => { - await editor.insertBlock( { - name: 'core/paragraph', - attributes: { - anchor: 'show-in-rest-false-binding', - content: 'fallback value', - metadata: { - bindings: { - content: { - source: 'core/post-meta', - args: { key: 'show_in_rest_false_field' }, - }, - }, - }, - }, - } ); - - const showInRestFalseBlock = editor.canvas.getByRole( - 'document', - { - name: 'Block: Paragraph', - } - ); - - await expect( showInRestFalseBlock ).toHaveAttribute( - 'contenteditable', - 'false' - ); - } ); - test( 'should show a selector for content', async ( { - editor, - page, - } ) => { - await editor.insertBlock( { - name: 'core/paragraph', - } ); - await page.getByLabel( 'Attributes options' ).click(); - const contentAttribute = page.getByRole( 'menuitemcheckbox', { - name: 'Show content', - } ); - await expect( contentAttribute ).toBeVisible(); - } ); - test( 'should use a selector to update the content', async ( { - editor, - page, - } ) => { - await editor.insertBlock( { - name: 'core/paragraph', - attributes: { - content: 'fallback value', - metadata: { - bindings: { - content: { - source: 'core/post-meta', - args: { key: 'undefined_field' }, - }, - }, - }, - }, - } ); - await page.getByRole( 'button', { name: 'content' } ).click(); - - await page - .getByRole( 'menuitemradio' ) - .filter( { hasText: 'text_custom_field' } ) - .click(); - const paragraphBlock = editor.canvas.getByRole( 'document', { - name: 'Block: Paragraph', - } ); - await expect( paragraphBlock ).toHaveText( - 'Value of the text custom field' - ); - } ); - } ); - - test.describe( 'Heading', () => { - test( 'should show the value of the custom field', async ( { - editor, - } ) => { - await editor.insertBlock( { - name: 'core/heading', - attributes: { - anchor: 'heading-binding', - content: 'heading default content', - metadata: { - bindings: { - content: { - source: 'core/post-meta', - args: { key: 'text_custom_field' }, - }, - }, - }, - }, - } ); - const headingBlock = editor.canvas.getByRole( 'document', { - name: 'Block: Heading', - } ); - await expect( headingBlock ).toHaveText( - 'Value of the text custom field' - ); - - // Check the frontend shows the value of the custom field. - const previewPage = await editor.openPreviewPage(); - await expect( - previewPage.locator( '#heading-binding' ) - ).toHaveText( 'Value of the text custom field' ); - } ); - - test( 'should add empty paragraph block when pressing enter', async ( { - editor, - page, - } ) => { - await editor.insertBlock( { - name: 'core/heading', - attributes: { - anchor: 'heading-binding', - content: 'heading default content', - metadata: { - bindings: { - content: { - source: 'core/post-meta', - args: { key: 'text_custom_field' }, - }, - }, - }, - }, - } ); - - // Select the heading and press Enter at the end of it. - const heading = editor.canvas.getByRole( 'document', { - name: 'Block: Heading', - } ); - await editor.selectBlocks( heading ); - await page.keyboard.press( 'End' ); - await page.keyboard.press( 'Enter' ); - // Can't use `editor.getBlocks` because it doesn't return the meta value shown in the editor. - const [ initialHeading, newEmptyParagraph ] = - await editor.canvas.locator( '[data-block]' ).all(); - // First block should be the original block. - await expect( initialHeading ).toHaveAttribute( - 'data-type', - 'core/heading' - ); - await expect( initialHeading ).toHaveText( - 'Value of the text custom field' - ); - // Second block should be an empty paragraph block. - await expect( newEmptyParagraph ).toHaveAttribute( - 'data-type', - 'core/paragraph' - ); - await expect( newEmptyParagraph ).toHaveText( '' ); - await expect( newEmptyParagraph ).toBeEditable(); - } ); - test( 'should show a selector for content', async ( { - editor, - page, - } ) => { - await editor.insertBlock( { - name: 'core/heading', - } ); - await page.getByLabel( 'Attributes options' ).click(); - const contentAttribute = page.getByRole( 'menuitemcheckbox', { - name: 'Show content', - } ); - await expect( contentAttribute ).toBeVisible(); - } ); - } ); - - test.describe( 'Button', () => { - test( 'should show the value of the custom field when text is bound', async ( { - editor, - } ) => { - await editor.insertBlock( { - name: 'core/buttons', - innerBlocks: [ - { - name: 'core/button', - attributes: { - anchor: 'button-text-binding', - text: 'button default text', - url: '#default-url', - metadata: { - bindings: { - text: { - source: 'core/post-meta', - args: { key: 'text_custom_field' }, - }, - }, - }, - }, - }, - ], - } ); - const buttonBlock = editor.canvas - .getByRole( 'document', { - name: 'Block: Button', - exact: true, - } ) - .getByRole( 'textbox' ); - await buttonBlock.click(); - await expect( buttonBlock ).toHaveText( - 'Value of the text custom field' - ); - - // Check the frontend shows the value of the custom field. - const previewPage = await editor.openPreviewPage(); - const buttonDom = previewPage.locator( - '#button-text-binding a' - ); - await expect( buttonDom ).toHaveText( - 'Value of the text custom field' - ); - await expect( buttonDom ).toHaveAttribute( - 'href', - '#default-url' - ); - } ); - - test( 'should use the value of the custom field when url is bound', async ( { - editor, - } ) => { - await editor.insertBlock( { - name: 'core/buttons', - innerBlocks: [ - { - name: 'core/button', - attributes: { - anchor: 'button-url-binding', - text: 'button default text', - url: '#default-url', - metadata: { - bindings: { - url: { - source: 'core/post-meta', - args: { key: 'url_custom_field' }, - }, - }, - }, - }, - }, - ], - } ); - - // Check the frontend shows the original value of the custom field. - const previewPage = await editor.openPreviewPage(); - const buttonDom = previewPage.locator( - '#button-url-binding a' - ); - await expect( buttonDom ).toHaveText( 'button default text' ); - await expect( buttonDom ).toHaveAttribute( - 'href', - '#url-custom-field' - ); - } ); - - test( 'should use the values of the custom fields when text and url are bound', async ( { - editor, - } ) => { - await editor.insertBlock( { - name: 'core/buttons', - innerBlocks: [ - { - name: 'core/button', - attributes: { - anchor: 'button-multiple-bindings', - text: 'button default text', - url: '#default-url', - metadata: { - bindings: { - text: { - source: 'core/post-meta', - args: { key: 'text_custom_field' }, - }, - url: { - source: 'core/post-meta', - args: { key: 'url_custom_field' }, - }, - }, - }, - }, - }, - ], - } ); - - // Check the frontend uses the values of the custom fields. - const previewPage = await editor.openPreviewPage(); - const buttonDom = previewPage.locator( - '#button-multiple-bindings a' - ); - await expect( buttonDom ).toHaveText( - 'Value of the text custom field' - ); - await expect( buttonDom ).toHaveAttribute( - 'href', - '#url-custom-field' - ); - } ); - - test( 'should add empty button block when pressing enter', async ( { - editor, - page, - } ) => { - await editor.insertBlock( { - name: 'core/buttons', - innerBlocks: [ - { - name: 'core/button', - attributes: { - anchor: 'button-text-binding', - text: 'button default text', - url: '#default-url', - metadata: { - bindings: { - text: { - source: 'core/post-meta', - args: { key: 'text_custom_field' }, - }, - }, - }, - }, - }, - ], - } ); - await editor.canvas - .getByRole( 'document', { - name: 'Block: Button', - exact: true, - } ) - .getByRole( 'textbox' ) - .click(); - await page.keyboard.press( 'End' ); - await page.keyboard.press( 'Enter' ); - const [ initialButton, newEmptyButton ] = await editor.canvas - .locator( '[data-type="core/button"]' ) - .all(); - // First block should be the original block. - await expect( initialButton ).toHaveText( - 'Value of the text custom field' - ); - // Second block should be an empty paragraph block. - await expect( newEmptyButton ).toHaveText( '' ); - await expect( newEmptyButton ).toBeEditable(); - } ); - test( 'should show a selector for url, text, linkTarget and rel', async ( { - editor, - page, - } ) => { - await editor.insertBlock( { - name: 'core/buttons', - innerBlocks: [ - { - name: 'core/button', - }, - ], - } ); - await editor.canvas - .getByRole( 'document', { - name: 'Block: Button', - exact: true, - } ) - .getByRole( 'textbox' ) - .click(); - await page - .getByRole( 'tabpanel', { - name: 'Settings', - } ) - .getByLabel( 'Attributes options' ) - .click(); - const urlAttribute = page.getByRole( 'menuitemcheckbox', { - name: 'Show url', - } ); - await expect( urlAttribute ).toBeVisible(); - const textAttribute = page.getByRole( 'menuitemcheckbox', { - name: 'Show text', - } ); - await expect( textAttribute ).toBeVisible(); - const linkTargetAttribute = page.getByRole( - 'menuitemcheckbox', - { - name: 'Show linkTarget', - } - ); - await expect( linkTargetAttribute ).toBeVisible(); - const relAttribute = page.getByRole( 'menuitemcheckbox', { - name: 'Show rel', - } ); - await expect( relAttribute ).toBeVisible(); - } ); - } ); - - test.describe( 'Image', () => { - test.beforeAll( async ( { requestUtils } ) => { - const customFieldMedia = await requestUtils.uploadMedia( - path.join( - './test/e2e/assets', - '1024x768_e2e_test_image_size.jpeg' - ) - ); - imageCustomFieldSrc = customFieldMedia.source_url; - } ); - - test.beforeEach( async ( { editor, page, requestUtils } ) => { - const postId = await editor.publishPost(); - await requestUtils.rest( { - method: 'POST', - path: '/wp/v2/posts/' + postId, - data: { - meta: { - url_custom_field: imageCustomFieldSrc, - }, - }, - } ); - await page.reload(); - } ); - test( 'should show the value of the custom field when url is bound', async ( { - editor, - } ) => { - await editor.insertBlock( { - name: 'core/image', - attributes: { - anchor: 'image-url-binding', - url: imagePlaceholderSrc, - alt: 'default alt value', - title: 'default title value', - metadata: { - bindings: { - url: { - source: 'core/post-meta', - args: { key: 'url_custom_field' }, - }, - }, - }, - }, - } ); - const imageBlockImg = editor.canvas - .getByRole( 'document', { - name: 'Block: Image', - } ) - .locator( 'img' ); - await expect( imageBlockImg ).toHaveAttribute( - 'src', - imageCustomFieldSrc - ); - - // Check the frontend uses the value of the custom field. - const previewPage = await editor.openPreviewPage(); - const imageDom = previewPage.locator( - '#image-url-binding img' - ); - await expect( imageDom ).toHaveAttribute( - 'src', - imageCustomFieldSrc - ); - await expect( imageDom ).toHaveAttribute( - 'alt', - 'default alt value' - ); - await expect( imageDom ).toHaveAttribute( - 'title', - 'default title value' - ); - } ); - - test( 'should show value of the custom field in the alt textarea when alt is bound', async ( { - editor, - page, - } ) => { - await editor.insertBlock( { - name: 'core/image', - attributes: { - anchor: 'image-alt-binding', - url: imagePlaceholderSrc, - alt: 'default alt value', - title: 'default title value', - metadata: { - bindings: { - alt: { - source: 'core/post-meta', - args: { key: 'text_custom_field' }, - }, - }, - }, - }, - } ); - const imageBlockImg = editor.canvas - .getByRole( 'document', { - name: 'Block: Image', - } ) - .locator( 'img' ); - await imageBlockImg.click(); - - // Image src is the placeholder. - await expect( imageBlockImg ).toHaveAttribute( - 'src', - imagePlaceholderSrc - ); - - // Alt textarea should have the custom field value. - const altValue = await page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Alternative text' ) - .inputValue(); - expect( altValue ).toBe( 'Value of the text custom field' ); - - // Check the frontend uses the value of the custom field. - const previewPage = await editor.openPreviewPage(); - const imageDom = previewPage.locator( - '#image-alt-binding img' - ); - await expect( imageDom ).toHaveAttribute( - 'src', - imagePlaceholderSrc - ); - await expect( imageDom ).toHaveAttribute( - 'alt', - 'Value of the text custom field' - ); - await expect( imageDom ).toHaveAttribute( - 'title', - 'default title value' - ); - } ); - - test( 'should show value of the custom field in the title input when title is bound', async ( { - editor, - page, - } ) => { - await editor.insertBlock( { - name: 'core/image', - attributes: { - anchor: 'image-title-binding', - url: imagePlaceholderSrc, - alt: 'default alt value', - title: 'default title value', - metadata: { - bindings: { - title: { - source: 'core/post-meta', - args: { key: 'text_custom_field' }, - }, - }, - }, - }, - } ); - const imageBlockImg = editor.canvas - .getByRole( 'document', { - name: 'Block: Image', - } ) - .locator( 'img' ); - await imageBlockImg.click(); - - // Image src is the placeholder. - await expect( imageBlockImg ).toHaveAttribute( - 'src', - imagePlaceholderSrc - ); - - // Title input should have the custom field value. - const advancedButton = page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByRole( 'button', { - name: 'Advanced', - } ); - const isAdvancedPanelOpen = - await advancedButton.getAttribute( 'aria-expanded' ); - if ( isAdvancedPanelOpen === 'false' ) { - await advancedButton.click(); - } - const titleValue = await page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Title attribute' ) - .inputValue(); - expect( titleValue ).toBe( 'Value of the text custom field' ); - - // Check the frontend uses the value of the custom field. - const previewPage = await editor.openPreviewPage(); - const imageDom = previewPage.locator( - '#image-title-binding img' - ); - await expect( imageDom ).toHaveAttribute( - 'src', - imagePlaceholderSrc - ); - await expect( imageDom ).toHaveAttribute( - 'alt', - 'default alt value' - ); - await expect( imageDom ).toHaveAttribute( - 'title', - 'Value of the text custom field' - ); - } ); - - test( 'Multiple bindings should show the value of the custom fields', async ( { - editor, - page, - } ) => { - await editor.insertBlock( { - name: 'core/image', - attributes: { - anchor: 'image-multiple-bindings', - url: imagePlaceholderSrc, - alt: 'default alt value', - title: 'default title value', - metadata: { - bindings: { - url: { - source: 'core/post-meta', - args: { key: 'url_custom_field' }, - }, - alt: { - source: 'core/post-meta', - args: { key: 'text_custom_field' }, - }, - }, - }, - }, - } ); - const imageBlockImg = editor.canvas - .getByRole( 'document', { - name: 'Block: Image', - } ) - .locator( 'img' ); - await imageBlockImg.click(); - - // Image src is the custom field value. - await expect( imageBlockImg ).toHaveAttribute( - 'src', - imageCustomFieldSrc - ); - - // Alt textarea should have the custom field value. - const altValue = await page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Alternative text' ) - .inputValue(); - expect( altValue ).toBe( 'Value of the text custom field' ); - - // Title input should have the original value. - const advancedButton = page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByRole( 'button', { - name: 'Advanced', - } ); - const isAdvancedPanelOpen = - await advancedButton.getAttribute( 'aria-expanded' ); - if ( isAdvancedPanelOpen === 'false' ) { - await advancedButton.click(); - } - const titleValue = await page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Title attribute' ) - .inputValue(); - expect( titleValue ).toBe( 'default title value' ); - - // Check the frontend uses the values of the custom fields. - const previewPage = await editor.openPreviewPage(); - const imageDom = previewPage.locator( - '#image-multiple-bindings img' - ); - await expect( imageDom ).toHaveAttribute( - 'src', - imageCustomFieldSrc - ); - await expect( imageDom ).toHaveAttribute( - 'alt', - 'Value of the text custom field' - ); - await expect( imageDom ).toHaveAttribute( - 'title', - 'default title value' - ); - } ); - test( 'should show a selector for url, id, title and alt', async ( { - editor, - page, - } ) => { - await editor.insertBlock( { - name: 'core/image', - } ); - await page - .getByRole( 'tabpanel', { - name: 'Settings', - } ) - .getByLabel( 'Attributes options' ) - .click(); - const urlAttribute = page.getByRole( 'menuitemcheckbox', { - name: 'Show url', - } ); - await expect( urlAttribute ).toBeVisible(); - const idAttribute = page.getByRole( 'menuitemcheckbox', { - name: 'Show id', - } ); - await expect( idAttribute ).toBeVisible(); - const titleAttribute = page.getByRole( 'menuitemcheckbox', { - name: 'Show title', - } ); - await expect( titleAttribute ).toBeVisible(); - const altAttribute = page.getByRole( 'menuitemcheckbox', { - name: 'Show alt', - } ); - await expect( altAttribute ).toBeVisible(); - } ); - } ); - - test.describe( 'Edit custom fields', () => { - test( 'should be possible to edit the value of the custom field from the paragraph', async ( { - editor, - } ) => { - await editor.insertBlock( { - name: 'core/paragraph', - attributes: { - anchor: 'paragraph-binding', - content: 'paragraph default content', - metadata: { - bindings: { - content: { - source: 'core/post-meta', - args: { key: 'text_custom_field' }, - }, - }, - }, - }, - } ); - const paragraphBlock = editor.canvas.getByRole( 'document', { - name: 'Block: Paragraph', - } ); - - await expect( paragraphBlock ).toHaveAttribute( - 'contenteditable', - 'true' - ); - await paragraphBlock.fill( 'new value' ); - // Check that the paragraph content attribute didn't change. - const [ paragraphBlockObject ] = await editor.getBlocks(); - expect( paragraphBlockObject.attributes.content ).toBe( - 'paragraph default content' - ); - // Check the value of the custom field is being updated by visiting the frontend. - const previewPage = await editor.openPreviewPage(); - await expect( - previewPage.locator( '#paragraph-binding' ) - ).toHaveText( 'new value' ); - } ); - - // Related issue: https://github.com/WordPress/gutenberg/issues/62347 - test( 'should be possible to use symbols and numbers as the custom field value', async ( { - editor, - } ) => { - await editor.insertBlock( { - name: 'core/paragraph', - attributes: { - anchor: 'paragraph-binding', - content: 'paragraph default content', - metadata: { - bindings: { - content: { - source: 'core/post-meta', - args: { key: 'text_custom_field' }, - }, - }, - }, - }, - } ); - const paragraphBlock = editor.canvas.getByRole( 'document', { - name: 'Block: Paragraph', - } ); - - await expect( paragraphBlock ).toHaveAttribute( - 'contenteditable', - 'true' - ); - await paragraphBlock.fill( '$10.00' ); - // Check the value of the custom field is being updated by visiting the frontend. - const previewPage = await editor.openPreviewPage(); - await expect( - previewPage.locator( '#paragraph-binding' ) - ).toHaveText( '$10.00' ); - } ); - - test( 'should be possible to edit the value of the url custom field from the button', async ( { - editor, - page, - pageUtils, - } ) => { - await editor.insertBlock( { - name: 'core/buttons', - innerBlocks: [ - { - name: 'core/button', - attributes: { - anchor: 'button-url-binding', - text: 'button default text', - url: '#default-url', - metadata: { - bindings: { - url: { - source: 'core/post-meta', - args: { key: 'url_custom_field' }, - }, - }, - }, - }, - }, - ], - } ); - - // Edit the url. - const buttonBlock = editor.canvas - .getByRole( 'document', { - name: 'Block: Button', - exact: true, - } ) - .getByRole( 'textbox' ); - await buttonBlock.click(); - await page - .getByRole( 'button', { name: 'Edit link', exact: true } ) - .click(); - await page - .getByPlaceholder( 'Search or type URL' ) - .fill( '#url-custom-field-modified' ); - await pageUtils.pressKeys( 'Enter' ); - - // Check that the button url attribute didn't change. - const [ buttonsObject ] = await editor.getBlocks(); - expect( buttonsObject.innerBlocks[ 0 ].attributes.url ).toBe( - '#default-url' - ); - // Check the value of the custom field is being updated by visiting the frontend. - const previewPage = await editor.openPreviewPage(); - await expect( - previewPage.locator( '#button-url-binding a' ) - ).toHaveAttribute( 'href', '#url-custom-field-modified' ); - } ); - - test( 'should be possible to edit the value of the url custom field from the image', async ( { - editor, - page, - pageUtils, - requestUtils, - } ) => { - const customFieldMedia = await requestUtils.uploadMedia( - path.join( - './test/e2e/assets', - '1024x768_e2e_test_image_size.jpeg' - ) - ); - imageCustomFieldSrc = customFieldMedia.source_url; - - await editor.insertBlock( { - name: 'core/image', - attributes: { - anchor: 'image-url-binding', - url: imagePlaceholderSrc, - alt: 'default alt value', - title: 'default title value', - metadata: { - bindings: { - url: { - source: 'core/post-meta', - args: { key: 'url_custom_field' }, - }, - }, - }, - }, - } ); - - // Edit image url. - await page - .getByRole( 'toolbar', { name: 'Block tools' } ) - .getByRole( 'button', { - name: 'Replace', - } ) - .click(); - await page - .getByRole( 'button', { name: 'Edit link', exact: true } ) - .click(); - await page - .getByPlaceholder( 'Search or type URL' ) - .fill( imageCustomFieldSrc ); - await pageUtils.pressKeys( 'Enter' ); - - // Check that the image url attribute didn't change and still uses the placeholder. - const [ imageBlockObject ] = await editor.getBlocks(); - expect( imageBlockObject.attributes.url ).toBe( - imagePlaceholderSrc - ); - - // Check the value of the custom field is being updated by visiting the frontend. - const previewPage = await editor.openPreviewPage(); - await expect( - previewPage.locator( '#image-url-binding img' ) - ).toHaveAttribute( 'src', imageCustomFieldSrc ); - } ); - - test( 'should be possible to edit the value of the text custom field from the image alt', async ( { - editor, - page, - } ) => { - await editor.insertBlock( { - name: 'core/image', - attributes: { - anchor: 'image-alt-binding', - url: imagePlaceholderSrc, - alt: 'default alt value', - metadata: { - bindings: { - alt: { - source: 'core/post-meta', - args: { key: 'text_custom_field' }, - }, - }, - }, - }, - } ); - const imageBlockImg = editor.canvas - .getByRole( 'document', { - name: 'Block: Image', - } ) - .locator( 'img' ); - await imageBlockImg.click(); - - // Edit the custom field value in the alt textarea. - const altInputArea = page - .getByRole( 'tabpanel', { name: 'Settings' } ) - .getByLabel( 'Alternative text' ); - await expect( altInputArea ).not.toHaveAttribute( 'readonly' ); - await altInputArea.fill( 'new value' ); - - // Check that the image alt attribute didn't change. - const [ imageBlockObject ] = await editor.getBlocks(); - expect( imageBlockObject.attributes.alt ).toBe( - 'default alt value' - ); - // Check the value of the custom field is being updated by visiting the frontend. - const previewPage = await editor.openPreviewPage(); - await expect( - previewPage.locator( '#image-alt-binding img' ) - ).toHaveAttribute( 'alt', 'new value' ); - } ); - } ); - } ); - - test.describe( 'Sources registration', () => { - test.beforeEach( async ( { admin } ) => { - await admin.createNewPost( { title: 'Test bindings' } ); - } ); - - test( 'should show the label of a source only registered in the server', async ( { - editor, - page, - } ) => { - await editor.insertBlock( { - name: 'core/paragraph', - attributes: { - metadata: { - bindings: { - content: { - source: 'core/server-source', - }, - }, - }, - }, - } ); - - const bindingsPanel = page.locator( - '.block-editor-bindings__panel' - ); - await expect( bindingsPanel ).toContainText( 'Server Source' ); - } ); - } ); -} ); diff --git a/test/e2e/specs/editor/various/block-bindings/custom-sources.spec.js b/test/e2e/specs/editor/various/block-bindings/custom-sources.spec.js new file mode 100644 index 00000000000000..d6563ce9cb5f5f --- /dev/null +++ b/test/e2e/specs/editor/various/block-bindings/custom-sources.spec.js @@ -0,0 +1,1204 @@ +/** + * External dependencies + */ +const path = require( 'path' ); +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +test.describe( 'Registered sources', () => { + let imagePlaceholderSrc; + let testingImgSrc; + test.beforeAll( async ( { requestUtils } ) => { + await requestUtils.activateTheme( + 'gutenberg-test-themes/block-bindings' + ); + await requestUtils.activatePlugin( 'gutenberg-test-block-bindings' ); + await requestUtils.deleteAllMedia(); + const placeholderMedia = await requestUtils.uploadMedia( + path.join( './test/e2e/assets', '10x10_e2e_test_image_z9T8jK.png' ) + ); + imagePlaceholderSrc = placeholderMedia.source_url; + + const testingImgMedia = await requestUtils.uploadMedia( + path.join( + './test/e2e/assets', + '1024x768_e2e_test_image_size.jpeg' + ) + ); + testingImgSrc = testingImgMedia.source_url; + } ); + + test.beforeEach( async ( { admin } ) => { + await admin.createNewPost( { title: 'Test bindings' } ); + } ); + + test.afterEach( async ( { requestUtils } ) => { + await requestUtils.deleteAllPosts(); + } ); + + test.afterAll( async ( { requestUtils } ) => { + await requestUtils.deleteAllMedia(); + await requestUtils.activateTheme( 'twentytwentyone' ); + await requestUtils.deactivatePlugin( 'gutenberg-test-block-bindings' ); + } ); + + test.describe( 'getValues', () => { + test( 'should show the returned value in paragraph content', async ( { + editor, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + anchor: 'connected-paragraph', + content: 'paragraph default content', + metadata: { + bindings: { + content: { + source: 'testing/complete-source', + args: { key: 'text_field' }, + }, + }, + }, + }, + } ); + const paragraphBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Paragraph', + } ); + await expect( paragraphBlock ).toHaveText( 'Text Field Value' ); + + // Check the frontend shows the value of the custom field. + const previewPage = await editor.openPreviewPage(); + await expect( + previewPage.locator( '#connected-paragraph' ) + ).toHaveText( 'Text Field Value' ); + } ); + test( 'should show the returned value in heading content', async ( { + editor, + } ) => { + await editor.insertBlock( { + name: 'core/heading', + attributes: { + anchor: 'connected-heading', + content: 'heading default content', + metadata: { + bindings: { + content: { + source: 'testing/complete-source', + args: { key: 'text_field' }, + }, + }, + }, + }, + } ); + const headingBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Heading', + } ); + await expect( headingBlock ).toHaveText( 'Text Field Value' ); + + // Check the frontend shows the value of the custom field. + const previewPage = await editor.openPreviewPage(); + await expect( + previewPage.locator( '#connected-heading' ) + ).toHaveText( 'Text Field Value' ); + } ); + test( 'should show the returned values in button attributes', async ( { + editor, + } ) => { + await editor.insertBlock( { + name: 'core/buttons', + innerBlocks: [ + { + name: 'core/button', + attributes: { + anchor: 'connected-button', + text: 'button default text', + url: '#default-url', + metadata: { + bindings: { + text: { + source: 'testing/complete-source', + args: { key: 'text_field' }, + }, + url: { + source: 'testing/complete-source', + args: { key: 'url_field' }, + }, + }, + }, + }, + }, + ], + } ); + + // Check the frontend uses the values of the custom fields. + const previewPage = await editor.openPreviewPage(); + const buttonDom = previewPage.locator( '#connected-button a' ); + await expect( buttonDom ).toHaveText( 'Text Field Value' ); + await expect( buttonDom ).toHaveAttribute( 'href', testingImgSrc ); + } ); + test( 'should show the returned values in image attributes', async ( { + editor, + page, + } ) => { + await editor.insertBlock( { + name: 'core/image', + attributes: { + anchor: 'connected-image', + url: imagePlaceholderSrc, + alt: 'default alt value', + title: 'default title value', + metadata: { + bindings: { + url: { + source: 'testing/complete-source', + args: { key: 'url_field' }, + }, + alt: { + source: 'testing/complete-source', + args: { key: 'text_field' }, + }, + }, + }, + }, + } ); + const imageBlockImg = editor.canvas + .getByRole( 'document', { + name: 'Block: Image', + } ) + .locator( 'img' ); + await imageBlockImg.click(); + + // Image src is the custom field value. + await expect( imageBlockImg ).toHaveAttribute( + 'src', + testingImgSrc + ); + + // Alt textarea should have the custom field value. + const altValue = await page + .getByRole( 'tabpanel', { name: 'Settings' } ) + .getByLabel( 'Alternative text' ) + .inputValue(); + expect( altValue ).toBe( 'Text Field Value' ); + + // Title input should have the original value. + const advancedButton = page + .getByRole( 'tabpanel', { name: 'Settings' } ) + .getByRole( 'button', { + name: 'Advanced', + } ); + const isAdvancedPanelOpen = + await advancedButton.getAttribute( 'aria-expanded' ); + if ( isAdvancedPanelOpen === 'false' ) { + await advancedButton.click(); + } + const titleValue = await page + .getByRole( 'tabpanel', { name: 'Settings' } ) + .getByLabel( 'Title attribute' ) + .inputValue(); + expect( titleValue ).toBe( 'default title value' ); + + // Check the frontend uses the values of the custom fields. + const previewPage = await editor.openPreviewPage(); + const imageDom = previewPage.locator( '#connected-image img' ); + await expect( imageDom ).toHaveAttribute( 'src', testingImgSrc ); + await expect( imageDom ).toHaveAttribute( + 'alt', + 'Text Field Value' + ); + await expect( imageDom ).toHaveAttribute( + 'title', + 'default title value' + ); + } ); + test( 'should fall back to source label when `getValues` is undefined', async ( { + editor, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + content: 'paragraph default content', + metadata: { + bindings: { + content: { + source: 'testing/server-only-source', + args: { key: 'text_field' }, + }, + }, + }, + }, + } ); + const paragraphBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Paragraph', + } ); + await expect( paragraphBlock ).toHaveText( 'Server Source' ); + } ); + test( 'should fall back to null when `getValues` is undefined in URL attributes', async ( { + editor, + } ) => { + await editor.insertBlock( { + name: 'core/image', + attributes: { + metadata: { + bindings: { + url: { + source: 'testing/server-only-source', + args: { key: 'url_field' }, + }, + }, + }, + }, + } ); + const imageBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Image', + } ); + await expect( + imageBlock.locator( '.components-placeholder__fieldset' ) + ).toHaveText( 'Connected to Server Source' ); + } ); + } ); + + test.describe( 'should lock editing', () => { + // Logic reused accross all the tests that check paragraph editing is locked. + async function testParagraphControlsAreLocked( { + source, + editor, + page, + } ) { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + content: 'paragraph default content', + metadata: { + bindings: { + content: { + source, + args: { key: 'text_field' }, + }, + }, + }, + }, + } ); + const paragraphBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Paragraph', + } ); + await paragraphBlock.click(); + + // Alignment controls exist. + await expect( + page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'button', { name: 'Align text' } ) + ).toBeVisible(); + + // Format controls don't exist. + await expect( + page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'button', { + name: 'Bold', + } ) + ).toBeHidden(); + + // Paragraph is not editable. + await expect( paragraphBlock ).toHaveAttribute( + 'contenteditable', + 'false' + ); + } + test.describe( 'canUserEditValue returns false', () => { + test( 'paragraph', async ( { editor, page } ) => { + await testParagraphControlsAreLocked( { + source: 'testing/can-user-edit-false', + editor, + page, + } ); + } ); + test( 'heading', async ( { editor, page } ) => { + await editor.insertBlock( { + name: 'core/heading', + attributes: { + content: 'heading default content', + metadata: { + bindings: { + content: { + source: 'testing/can-user-edit-false', + args: { key: 'text_field' }, + }, + }, + }, + }, + } ); + const headingBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Heading', + } ); + await headingBlock.click(); + + // Alignment controls exist. + await expect( + page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'button', { name: 'Align text' } ) + ).toBeVisible(); + + // Format controls don't exist. + await expect( + page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'button', { + name: 'Bold', + } ) + ).toBeHidden(); + + // Heading is not editable. + await expect( headingBlock ).toHaveAttribute( + 'contenteditable', + 'false' + ); + } ); + test( 'button', async ( { editor, page } ) => { + await editor.insertBlock( { + name: 'core/buttons', + innerBlocks: [ + { + name: 'core/button', + attributes: { + text: 'button default text', + url: '#default-url', + metadata: { + bindings: { + text: { + source: 'testing/can-user-edit-false', + args: { key: 'text_field' }, + }, + url: { + source: 'testing/can-user-edit-false', + args: { key: 'url_field' }, + }, + }, + }, + }, + }, + ], + } ); + const buttonBlock = editor.canvas + .getByRole( 'document', { + name: 'Block: Button', + exact: true, + } ) + .getByRole( 'textbox' ); + await buttonBlock.click(); + + // Alignment controls are visible. + await expect( + page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'button', { name: 'Align text' } ) + ).toBeVisible(); + + // Format controls don't exist. + await expect( + page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'button', { + name: 'Bold', + } ) + ).toBeHidden(); + + // Button is not editable. + await expect( buttonBlock ).toHaveAttribute( + 'contenteditable', + 'false' + ); + + // Link controls don't exist. + await expect( + page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'button', { name: 'Link' } ) + ).toBeHidden(); + await expect( + page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'button', { name: 'Unlink' } ) + ).toBeHidden(); + } ); + test( 'image', async ( { editor, page } ) => { + await editor.insertBlock( { + name: 'core/image', + attributes: { + url: imagePlaceholderSrc, + alt: 'default alt value', + title: 'default title value', + metadata: { + bindings: { + url: { + source: 'testing/can-user-edit-false', + args: { key: 'url_field' }, + }, + alt: { + source: 'testing/can-user-edit-false', + args: { key: 'text_field' }, + }, + title: { + source: 'testing/can-user-edit-false', + args: { key: 'text_field' }, + }, + }, + }, + }, + } ); + const imageBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Image', + } ); + await imageBlock.click(); + + // Replace controls don't exist. + await expect( + page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'button', { + name: 'Replace', + } ) + ).toBeHidden(); + + // Image placeholder doesn't show the upload button. + await expect( + imageBlock.getByRole( 'button', { name: 'Upload' } ) + ).toBeHidden(); + + // Alt textarea is disabled and with the custom field value. + await expect( + page + .getByRole( 'tabpanel', { name: 'Settings' } ) + .getByLabel( 'Alternative text' ) + ).toHaveAttribute( 'readonly' ); + const altValue = await page + .getByRole( 'tabpanel', { name: 'Settings' } ) + .getByLabel( 'Alternative text' ) + .inputValue(); + expect( altValue ).toBe( 'Text Field Value' ); + + // Title input is enabled and with the original value. + await page + .getByRole( 'tabpanel', { name: 'Settings' } ) + .getByRole( 'button', { name: 'Advanced' } ) + .click(); + await expect( + page + .getByRole( 'tabpanel', { name: 'Settings' } ) + .getByLabel( 'Title attribute' ) + ).toHaveAttribute( 'readonly' ); + const titleValue = await page + .getByRole( 'tabpanel', { name: 'Settings' } ) + .getByLabel( 'Title attribute' ) + .inputValue(); + expect( titleValue ).toBe( 'Text Field Value' ); + } ); + } ); + // The following tests just check the paragraph and assume is the case for the rest of the blocks. + test( 'canUserEditValue is not defined', async ( { editor, page } ) => { + await testParagraphControlsAreLocked( { + source: 'testing/can-user-edit-undefined', + editor, + page, + } ); + } ); + test( 'setValues is not defined', async ( { editor, page } ) => { + await testParagraphControlsAreLocked( { + source: 'testing/complete-source-undefined', + editor, + page, + } ); + } ); + test( 'source is not defined', async ( { editor, page } ) => { + await testParagraphControlsAreLocked( { + source: 'testing/undefined-source', + editor, + page, + } ); + } ); + } ); + + // Use `core/post-meta` source to test editing to avoid overcomplicating custom sources. + // It needs a source that can be consumed and edited from the server and the editor. + test.describe( 'setValues', () => { + test( 'should be possible to edit the value from paragraph content', async ( { + editor, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + anchor: 'connected-paragraph', + content: 'paragraph default content', + metadata: { + bindings: { + content: { + source: 'core/post-meta', + args: { key: 'text_custom_field' }, + }, + }, + }, + }, + } ); + const paragraphBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Paragraph', + } ); + + await expect( paragraphBlock ).toHaveAttribute( + 'contenteditable', + 'true' + ); + await paragraphBlock.fill( 'new value' ); + // Check that the paragraph content attribute didn't change. + const [ paragraphBlockObject ] = await editor.getBlocks(); + expect( paragraphBlockObject.attributes.content ).toBe( + 'paragraph default content' + ); + // Check the value of the custom field is being updated by visiting the frontend. + const previewPage = await editor.openPreviewPage(); + await expect( + previewPage.locator( '#connected-paragraph' ) + ).toHaveText( 'new value' ); + } ); + // Related issue: https://github.com/WordPress/gutenberg/issues/62347 + test( 'should be possible to use symbols and numbers as the custom field value', async ( { + editor, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + anchor: 'paragraph-binding', + content: 'paragraph default content', + metadata: { + bindings: { + content: { + source: 'core/post-meta', + args: { key: 'text_custom_field' }, + }, + }, + }, + }, + } ); + const paragraphBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Paragraph', + } ); + + await expect( paragraphBlock ).toHaveAttribute( + 'contenteditable', + 'true' + ); + await paragraphBlock.fill( '$10.00' ); + // Check the value of the custom field is being updated by visiting the frontend. + const previewPage = await editor.openPreviewPage(); + await expect( + previewPage.locator( '#paragraph-binding' ) + ).toHaveText( '$10.00' ); + } ); + test( 'should be possible to edit the value of the url custom field from the button', async ( { + editor, + page, + pageUtils, + } ) => { + await editor.insertBlock( { + name: 'core/buttons', + innerBlocks: [ + { + name: 'core/button', + attributes: { + anchor: 'button-url-binding', + text: 'button default text', + url: '#default-url', + metadata: { + bindings: { + url: { + source: 'core/post-meta', + args: { key: 'url_custom_field' }, + }, + }, + }, + }, + }, + ], + } ); + + // Edit the url. + const buttonBlock = editor.canvas + .getByRole( 'document', { + name: 'Block: Button', + exact: true, + } ) + .getByRole( 'textbox' ); + await buttonBlock.click(); + await page + .getByRole( 'button', { name: 'Edit link', exact: true } ) + .click(); + await page + .getByPlaceholder( 'Search or type URL' ) + .fill( '#url-custom-field-modified' ); + await pageUtils.pressKeys( 'Enter' ); + + // Check that the button url attribute didn't change. + const [ buttonsObject ] = await editor.getBlocks(); + expect( buttonsObject.innerBlocks[ 0 ].attributes.url ).toBe( + '#default-url' + ); + // Check the value of the custom field is being updated by visiting the frontend. + const previewPage = await editor.openPreviewPage(); + await expect( + previewPage.locator( '#button-url-binding a' ) + ).toHaveAttribute( 'href', '#url-custom-field-modified' ); + } ); + test( 'should be possible to edit the value of the url custom field from the image', async ( { + editor, + page, + pageUtils, + requestUtils, + } ) => { + const customFieldMedia = await requestUtils.uploadMedia( + path.join( + './test/e2e/assets', + '1024x768_e2e_test_image_size.jpeg' + ) + ); + testingImgSrc = customFieldMedia.source_url; + + await editor.insertBlock( { + name: 'core/image', + attributes: { + anchor: 'image-url-binding', + url: imagePlaceholderSrc, + alt: 'default alt value', + title: 'default title value', + metadata: { + bindings: { + url: { + source: 'core/post-meta', + args: { key: 'url_custom_field' }, + }, + }, + }, + }, + } ); + + // Edit image url. + await page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'button', { + name: 'Replace', + } ) + .click(); + await page + .getByRole( 'button', { name: 'Edit link', exact: true } ) + .click(); + await page + .getByPlaceholder( 'Search or type URL' ) + .fill( testingImgSrc ); + await pageUtils.pressKeys( 'Enter' ); + + // Check that the image url attribute didn't change and still uses the placeholder. + const [ imageBlockObject ] = await editor.getBlocks(); + expect( imageBlockObject.attributes.url ).toBe( + imagePlaceholderSrc + ); + + // Check the value of the custom field is being updated by visiting the frontend. + const previewPage = await editor.openPreviewPage(); + await expect( + previewPage.locator( '#image-url-binding img' ) + ).toHaveAttribute( 'src', testingImgSrc ); + } ); + test( 'should be possible to edit the value of the text custom field from the image alt', async ( { + editor, + page, + } ) => { + await editor.insertBlock( { + name: 'core/image', + attributes: { + anchor: 'image-alt-binding', + url: imagePlaceholderSrc, + alt: 'default alt value', + metadata: { + bindings: { + alt: { + source: 'core/post-meta', + args: { key: 'text_custom_field' }, + }, + }, + }, + }, + } ); + const imageBlockImg = editor.canvas + .getByRole( 'document', { + name: 'Block: Image', + } ) + .locator( 'img' ); + await imageBlockImg.click(); + + // Edit the custom field value in the alt textarea. + const altInputArea = page + .getByRole( 'tabpanel', { name: 'Settings' } ) + .getByLabel( 'Alternative text' ); + await expect( altInputArea ).not.toHaveAttribute( 'readonly' ); + await altInputArea.fill( 'new value' ); + + // Check that the image alt attribute didn't change. + const [ imageBlockObject ] = await editor.getBlocks(); + expect( imageBlockObject.attributes.alt ).toBe( + 'default alt value' + ); + // Check the value of the custom field is being updated by visiting the frontend. + const previewPage = await editor.openPreviewPage(); + await expect( + previewPage.locator( '#image-alt-binding img' ) + ).toHaveAttribute( 'alt', 'new value' ); + } ); + } ); + + test.describe( 'getFieldsList', () => { + test( 'should be possible to update attribute value through bindings UI', async ( { + editor, + page, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + } ); + await page.getByLabel( 'Attributes options' ).click(); + await page + .getByRole( 'menuitemcheckbox', { + name: 'Show content', + } ) + .click(); + await page.getByRole( 'button', { name: 'content' } ).click(); + await page + .getByRole( 'menuitemradio' ) + .filter( { hasText: 'Text Field Label' } ) + .click(); + const paragraphBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Paragraph', + } ); + await expect( paragraphBlock ).toHaveText( 'Text Field Value' ); + } ); + test( 'should be possible to connect the paragraph content', async ( { + editor, + page, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + } ); + await page.getByLabel( 'Attributes options' ).click(); + const contentAttribute = page.getByRole( 'menuitemcheckbox', { + name: 'Show content', + } ); + await expect( contentAttribute ).toBeVisible(); + } ); + test( 'should be possible to connect the heading content', async ( { + editor, + page, + } ) => { + await editor.insertBlock( { + name: 'core/heading', + } ); + await page.getByLabel( 'Attributes options' ).click(); + const contentAttribute = page.getByRole( 'menuitemcheckbox', { + name: 'Show content', + } ); + await expect( contentAttribute ).toBeVisible(); + } ); + test( 'should be possible to connect the button supported attributes', async ( { + editor, + page, + } ) => { + await editor.insertBlock( { + name: 'core/buttons', + innerBlocks: [ + { + name: 'core/button', + }, + ], + } ); + await editor.canvas + .getByRole( 'document', { + name: 'Block: Button', + exact: true, + } ) + .getByRole( 'textbox' ) + .click(); + await page + .getByRole( 'tabpanel', { + name: 'Settings', + } ) + .getByLabel( 'Attributes options' ) + .click(); + const urlAttribute = page.getByRole( 'menuitemcheckbox', { + name: 'Show url', + } ); + await expect( urlAttribute ).toBeVisible(); + const textAttribute = page.getByRole( 'menuitemcheckbox', { + name: 'Show text', + } ); + await expect( textAttribute ).toBeVisible(); + const linkTargetAttribute = page.getByRole( 'menuitemcheckbox', { + name: 'Show linkTarget', + } ); + await expect( linkTargetAttribute ).toBeVisible(); + const relAttribute = page.getByRole( 'menuitemcheckbox', { + name: 'Show rel', + } ); + await expect( relAttribute ).toBeVisible(); + // Check not supported attributes are not included. + const tagNameAttribute = page.getByRole( 'menuitemcheckbox', { + name: 'Show tagName', + } ); + await expect( tagNameAttribute ).toBeHidden(); + } ); + test( 'should be possible to connect the image supported attributes', async ( { + editor, + page, + } ) => { + await editor.insertBlock( { + name: 'core/image', + } ); + await page + .getByRole( 'tabpanel', { + name: 'Settings', + } ) + .getByLabel( 'Attributes options' ) + .click(); + const urlAttribute = page.getByRole( 'menuitemcheckbox', { + name: 'Show url', + } ); + await expect( urlAttribute ).toBeVisible(); + const idAttribute = page.getByRole( 'menuitemcheckbox', { + name: 'Show id', + } ); + await expect( idAttribute ).toBeVisible(); + const titleAttribute = page.getByRole( 'menuitemcheckbox', { + name: 'Show title', + } ); + await expect( titleAttribute ).toBeVisible(); + const altAttribute = page.getByRole( 'menuitemcheckbox', { + name: 'Show alt', + } ); + await expect( altAttribute ).toBeVisible(); + // Check not supported attributes are not included. + const linkClassAttribute = page.getByRole( 'menuitemcheckbox', { + name: 'Show linkClass', + } ); + await expect( linkClassAttribute ).toBeHidden(); + } ); + test( 'should show all the available fields in the dropdown UI', async ( { + editor, + page, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + content: 'default value', + metadata: { + bindings: { + content: { + source: 'testing/complete-source', + args: { key: 'text_field' }, + }, + }, + }, + }, + } ); + await page.getByRole( 'button', { name: 'content' } ).click(); + const textField = page + .getByRole( 'menuitemradio' ) + .filter( { hasText: 'Text Field Label' } ); + await expect( textField ).toBeVisible(); + await expect( textField ).toBeChecked(); + const urlField = page + .getByRole( 'menuitemradio' ) + .filter( { hasText: 'URL Field Label' } ); + await expect( urlField ).toBeVisible(); + await expect( urlField ).not.toBeChecked(); + } ); + test( 'should show the connected fields in the attributes panel', async ( { + editor, + page, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + content: 'default value', + metadata: { + bindings: { + content: { + source: 'testing/complete-source', + args: { key: 'text_field' }, + }, + }, + }, + }, + } ); + const contentButton = page.getByRole( 'button', { + name: 'content', + } ); + await expect( contentButton ).toContainText( 'Text Field Label' ); + } ); + } ); + + test.describe( 'RichText workflows', () => { + test( 'should add empty paragraph block when pressing enter in paragraph', async ( { + editor, + page, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + content: 'paragraph default content', + metadata: { + bindings: { + content: { + source: 'testing/complete-source', + args: { key: 'text_field' }, + }, + }, + }, + }, + } ); + // Select the paragraph and press Enter at the end of it. + const paragraph = editor.canvas.getByRole( 'document', { + name: 'Block: Paragraph', + } ); + await editor.selectBlocks( paragraph ); + await page.keyboard.press( 'End' ); + await page.keyboard.press( 'Enter' ); + const [ initialParagraph, newEmptyParagraph ] = await editor.canvas + .locator( '[data-type="core/paragraph"]' ) + .all(); + await expect( initialParagraph ).toHaveText( 'Text Field Value' ); + await expect( newEmptyParagraph ).toHaveText( '' ); + await expect( newEmptyParagraph ).toBeEditable(); + } ); + test( 'should add empty paragraph block when pressing enter in heading', async ( { + editor, + page, + } ) => { + await editor.insertBlock( { + name: 'core/heading', + attributes: { + anchor: 'heading-binding', + content: 'heading default content', + metadata: { + bindings: { + content: { + source: 'testing/complete-source', + args: { key: 'text_field' }, + }, + }, + }, + }, + } ); + + // Select the heading and press Enter at the end of it. + const heading = editor.canvas.getByRole( 'document', { + name: 'Block: Heading', + } ); + await editor.selectBlocks( heading ); + await page.keyboard.press( 'End' ); + await page.keyboard.press( 'Enter' ); + // Can't use `editor.getBlocks` because it doesn't return the meta value shown in the editor. + const [ initialHeading, newEmptyParagraph ] = await editor.canvas + .locator( '[data-block]' ) + .all(); + // First block should be the original block. + await expect( initialHeading ).toHaveAttribute( + 'data-type', + 'core/heading' + ); + await expect( initialHeading ).toHaveText( 'Text Field Value' ); + // Second block should be an empty paragraph block. + await expect( newEmptyParagraph ).toHaveAttribute( + 'data-type', + 'core/paragraph' + ); + await expect( newEmptyParagraph ).toHaveText( '' ); + await expect( newEmptyParagraph ).toBeEditable(); + } ); + test( 'should add empty button block when pressing enter in button', async ( { + editor, + page, + } ) => { + await editor.insertBlock( { + name: 'core/buttons', + innerBlocks: [ + { + name: 'core/button', + attributes: { + anchor: 'button-text-binding', + text: 'button default text', + url: '#default-url', + metadata: { + bindings: { + text: { + source: 'testing/complete-source', + args: { key: 'text_field' }, + }, + }, + }, + }, + }, + ], + } ); + await editor.canvas + .getByRole( 'document', { + name: 'Block: Button', + exact: true, + } ) + .getByRole( 'textbox' ) + .click(); + await page.keyboard.press( 'End' ); + await page.keyboard.press( 'Enter' ); + const [ initialButton, newEmptyButton ] = await editor.canvas + .locator( '[data-type="core/button"]' ) + .all(); + // First block should be the original block. + await expect( initialButton ).toHaveText( 'Text Field Value' ); + // Second block should be an empty paragraph block. + await expect( newEmptyButton ).toHaveText( '' ); + await expect( newEmptyButton ).toBeEditable(); + } ); + test( 'should show placeholder prompt when value is empty and can edit', async ( { + editor, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + content: 'paragraph default content', + metadata: { + bindings: { + content: { + source: 'testing/complete-source', + args: { key: 'empty_field' }, + }, + }, + }, + }, + } ); + + const paragraphBlock = editor.canvas.getByRole( 'document', { + // Aria-label is changed for empty paragraphs. + name: 'Empty empty_field; start writing to edit its value', + } ); + + await expect( paragraphBlock ).toBeEmpty(); + + const placeholder = paragraphBlock.locator( 'span' ); + await expect( placeholder ).toHaveAttribute( + 'data-rich-text-placeholder', + 'Add Empty Field Label' + ); + } ); + test( 'should show source label when value is empty, cannot edit, and `getFieldsList` is undefined', async ( { + editor, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + content: 'paragraph default content', + metadata: { + bindings: { + content: { + source: 'testing/can-user-edit-false', + args: { key: 'empty_field' }, + }, + }, + }, + }, + } ); + const paragraphBlock = editor.canvas.getByRole( 'document', { + // Aria-label is changed for empty paragraphs. + name: 'empty_field', + } ); + await expect( paragraphBlock ).toBeEmpty(); + const placeholder = paragraphBlock.locator( 'span' ); + await expect( placeholder ).toHaveAttribute( + 'data-rich-text-placeholder', + 'Can User Edit: False' + ); + } ); + test( 'should show placeholder attribute over bindings placeholder', async ( { + editor, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + placeholder: 'My custom placeholder', + content: 'paragraph default content', + metadata: { + bindings: { + content: { + source: 'testing/complete-source', + args: { key: 'empty_field' }, + }, + }, + }, + }, + } ); + const paragraphBlock = editor.canvas.getByRole( 'document', { + // Aria-label is changed for empty paragraphs. + name: 'empty_field', + } ); + + await expect( paragraphBlock ).toBeEmpty(); + + const placeholder = paragraphBlock.locator( 'span' ); + await expect( placeholder ).toHaveAttribute( + 'data-rich-text-placeholder', + 'My custom placeholder' + ); + } ); + } ); + + test( 'should show the label of a source only registered in the server in blocks connected', async ( { + editor, + page, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + metadata: { + bindings: { + content: { + source: 'testing/server-only-source', + }, + }, + }, + }, + } ); + + const contentButton = page.getByRole( 'button', { + name: 'content', + } ); + await expect( contentButton ).toContainText( 'Server Source' ); + } ); + test( 'should show an "Invalid source" warning for not registered sources', async ( { + editor, + page, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + metadata: { + bindings: { + content: { + source: 'testing/undefined-source', + }, + }, + }, + }, + } ); + + const contentButton = page.getByRole( 'button', { + name: 'content', + } ); + await expect( contentButton ).toContainText( 'Invalid source' ); + } ); +} ); diff --git a/test/e2e/specs/editor/various/block-bindings/post-meta.spec.js b/test/e2e/specs/editor/various/block-bindings/post-meta.spec.js new file mode 100644 index 00000000000000..32334bfc777f2a --- /dev/null +++ b/test/e2e/specs/editor/various/block-bindings/post-meta.spec.js @@ -0,0 +1,600 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +test.describe( 'Post Meta source', () => { + test.beforeAll( async ( { requestUtils } ) => { + await requestUtils.activateTheme( + 'gutenberg-test-themes/block-bindings' + ); + await requestUtils.activatePlugin( 'gutenberg-test-block-bindings' ); + } ); + + test.afterEach( async ( { requestUtils } ) => { + await requestUtils.deleteAllPosts(); + } ); + + test.afterAll( async ( { requestUtils } ) => { + await requestUtils.deleteAllMedia(); + await requestUtils.activateTheme( 'twentytwentyone' ); + await requestUtils.deactivatePlugin( 'gutenberg-test-block-bindings' ); + } ); + + test.describe( 'Movie CPT template', () => { + test.beforeEach( async ( { admin, editor } ) => { + await admin.visitSiteEditor( { + postId: 'gutenberg-test-themes/block-bindings//single-movie', + postType: 'wp_template', + canvas: 'edit', + } ); + await editor.openDocumentSettingsSidebar(); + } ); + + test.describe( 'Block attributes values', () => { + test( 'should not be possible to edit connected blocks', async ( { + editor, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + content: 'fallback content', + metadata: { + bindings: { + content: { + source: 'core/post-meta', + args: { + key: 'movie_field', + }, + }, + }, + }, + }, + } ); + const paragraphBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Paragraph', + } ); + await expect( paragraphBlock ).toHaveAttribute( + 'contenteditable', + 'false' + ); + } ); + test( 'should show the default value if it is defined', async ( { + editor, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + content: 'fallback content', + metadata: { + bindings: { + content: { + source: 'core/post-meta', + args: { + key: 'movie_field', + }, + }, + }, + }, + }, + } ); + const paragraphBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Paragraph', + } ); + await expect( paragraphBlock ).toHaveText( + 'Movie field default value' + ); + } ); + test( 'should fall back to the field label if the default value is not defined', async ( { + editor, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + content: 'fallback content', + metadata: { + bindings: { + content: { + source: 'core/post-meta', + args: { + key: 'field_with_only_label', + }, + }, + }, + }, + }, + } ); + const paragraphBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Paragraph', + } ); + await expect( paragraphBlock ).toHaveText( + 'Field with only label' + ); + } ); + test( 'should fall back to the field key if the field label is not defined', async ( { + editor, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + content: 'fallback content', + metadata: { + bindings: { + content: { + source: 'core/post-meta', + args: { + key: 'field_without_label_or_default', + }, + }, + }, + }, + }, + } ); + const paragraphBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Paragraph', + } ); + await expect( paragraphBlock ).toHaveText( + 'field_without_label_or_default' + ); + } ); + } ); + + test.describe( 'Attributes panel', () => { + test( 'should show the field label if it is defined', async ( { + editor, + page, + } ) => { + /** + * Create connection manually until this issue is solved: + * https://github.com/WordPress/gutenberg/pull/65604 + * + * Once solved, block with the binding can be directly inserted. + */ + await editor.insertBlock( { + name: 'core/paragraph', + } ); + await page.getByLabel( 'Attributes options' ).click(); + await page + .getByRole( 'menuitemcheckbox', { + name: 'Show content', + } ) + .click(); + const contentBinding = page.getByRole( 'button', { + name: 'content', + } ); + await contentBinding.click(); + await page + .getByRole( 'menuitemradio' ) + .filter( { hasText: 'Movie field label' } ) + .click(); + await expect( contentBinding ).toContainText( + 'Movie field label' + ); + } ); + test( 'should fall back to the field key if the field label is not defined', async ( { + editor, + page, + } ) => { + /** + * Create connection manually until this issue is solved: + * https://github.com/WordPress/gutenberg/pull/65604 + * + * Once solved, block with the binding can be directly inserted. + */ + await editor.insertBlock( { + name: 'core/paragraph', + } ); + await page.getByLabel( 'Attributes options' ).click(); + await page + .getByRole( 'menuitemcheckbox', { + name: 'Show content', + } ) + .click(); + const contentBinding = page.getByRole( 'button', { + name: 'content', + } ); + await contentBinding.click(); + await page + .getByRole( 'menuitemradio' ) + .filter( { hasText: 'field_without_label_or_default' } ) + .click(); + await expect( contentBinding ).toContainText( + 'field_without_label_or_default' + ); + } ); + } ); + + test.describe( 'Fields list dropdown', () => { + // Insert block and open the dropdown for every test. + test.beforeEach( async ( { editor, page } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + } ); + await page.getByLabel( 'Attributes options' ).click(); + await page + .getByRole( 'menuitemcheckbox', { + name: 'Show content', + } ) + .click(); + await page + .getByRole( 'button', { + name: 'content', + } ) + .click(); + } ); + + test( 'should include movie fields in UI to connect attributes', async ( { + page, + } ) => { + const movieField = page + .getByRole( 'menuitemradio' ) + .filter( { hasText: 'Movie field label' } ); + await expect( movieField ).toBeVisible(); + } ); + test( 'should include global fields in UI to connect attributes', async ( { + page, + } ) => { + const globalField = page + .getByRole( 'menuitemradio' ) + .filter( { hasText: 'text_custom_field' } ); + await expect( globalField ).toBeVisible(); + } ); + test( 'should not include protected fields', async ( { page } ) => { + // Ensure the fields have loaded by checking the field is visible. + const globalField = page + .getByRole( 'menuitemradio' ) + .filter( { hasText: 'text_custom_field' } ); + await expect( globalField ).toBeVisible(); + // Check the protected fields are not visible. + const protectedField = page + .getByRole( 'menuitemradio' ) + .filter( { hasText: '_protected_field' } ); + await expect( protectedField ).toBeHidden(); + const showInRestFalseField = page + .getByRole( 'menuitemradio' ) + .filter( { hasText: 'show_in_rest_false_field' } ); + await expect( showInRestFalseField ).toBeHidden(); + } ); + test( 'should show the default value if it is defined', async ( { + page, + } ) => { + const fieldButton = page + .getByRole( 'menuitemradio' ) + .filter( { hasText: 'Movie field label' } ); + await expect( fieldButton ).toContainText( + 'Movie field default value' + ); + } ); + test( 'should not show anything if the default value is not defined', async ( { + page, + } ) => { + const fieldButton = page + .getByRole( 'menuitemradio' ) + .filter( { hasText: 'Field with only label' } ); + // Check it only contains the field label. + await expect( fieldButton ).toHaveText( + 'Field with only label' + ); + } ); + } ); + } ); + + test.describe( 'Custom template', () => { + test.beforeEach( async ( { admin, editor } ) => { + await admin.visitSiteEditor( { + postId: 'gutenberg-test-themes/block-bindings//custom-template', + postType: 'wp_template', + canvas: 'edit', + } ); + await editor.openDocumentSettingsSidebar(); + } ); + + test( 'should not include post meta fields in UI to connect attributes', async ( { + editor, + page, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + content: 'fallback content', + metadata: { + bindings: { + content: { + source: 'core/post-meta', + args: { + key: 'text_custom_field', + }, + }, + }, + }, + }, + } ); + await page + .getByRole( 'button', { + name: 'content', + } ) + .click(); + // Check the fields registered by other sources are there. + const customSourceField = page + .getByRole( 'menuitemradio' ) + .filter( { hasText: 'Text Field Label' } ); + await expect( customSourceField ).toBeVisible(); + // Check the post meta fields are not visible. + const globalField = page + .getByRole( 'menuitemradio' ) + .filter( { hasText: 'text_custom_field' } ); + await expect( globalField ).toBeHidden(); + const movieField = page + .getByRole( 'menuitemradio' ) + .filter( { hasText: 'Movie field label' } ); + await expect( movieField ).toBeHidden(); + } ); + test( 'should show the key in attributes connected to post meta', async ( { + editor, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + content: 'fallback content', + metadata: { + bindings: { + content: { + source: 'core/post-meta', + args: { + key: 'text_custom_field', + }, + }, + }, + }, + }, + } ); + const paragraphBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Paragraph', + } ); + await expect( paragraphBlock ).toHaveText( 'text_custom_field' ); + } ); + } ); + + test.describe( 'Movie CPT post', () => { + test.beforeEach( async ( { admin } ) => { + // CHECK HOW TO CREATE A MOVIE. + await admin.createNewPost( { + postType: 'movie', + title: 'Test bindings', + } ); + } ); + + test( 'should show the custom field value of that specific post', async ( { + editor, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + anchor: 'connected-paragraph', + content: 'fallback content', + metadata: { + bindings: { + content: { + source: 'core/post-meta', + args: { + key: 'movie_field', + }, + }, + }, + }, + }, + } ); + const paragraphBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Paragraph', + } ); + await expect( paragraphBlock ).toHaveText( + 'Movie field default value' + ); + // Check the frontend shows the value of the custom field. + const previewPage = await editor.openPreviewPage(); + await expect( + previewPage.locator( '#connected-paragraph' ) + ).toHaveText( 'Movie field default value' ); + } ); + test( 'should fall back to the key when custom field is not accessible', async ( { + editor, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + content: 'fallback content', + metadata: { + bindings: { + content: { + source: 'core/post-meta', + args: { + key: 'unaccessible_field', + }, + }, + }, + }, + }, + } ); + const paragraphBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Paragraph', + } ); + await expect( paragraphBlock ).toHaveText( 'unaccessible_field' ); + await expect( paragraphBlock ).toHaveAttribute( + 'contenteditable', + 'false' + ); + } ); + test( 'should not show or edit the value of a protected field', async ( { + editor, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + content: 'fallback content', + metadata: { + bindings: { + content: { + source: 'core/post-meta', + args: { + key: '_protected_field', + }, + }, + }, + }, + }, + } ); + const paragraphBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Paragraph', + } ); + await expect( paragraphBlock ).toHaveText( '_protected_field' ); + await expect( paragraphBlock ).toHaveAttribute( + 'contenteditable', + 'false' + ); + } ); + test( 'should not show or edit the value of a field with `show_in_rest` set to false', async ( { + editor, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + content: 'fallback content', + metadata: { + bindings: { + content: { + source: 'core/post-meta', + args: { + key: 'show_in_rest_false_field', + }, + }, + }, + }, + }, + } ); + const paragraphBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Paragraph', + } ); + await expect( paragraphBlock ).toHaveText( + 'show_in_rest_false_field' + ); + await expect( paragraphBlock ).toHaveAttribute( + 'contenteditable', + 'false' + ); + } ); + test( 'should be possible to edit the value of the connected custom fields', async ( { + editor, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + anchor: 'connected-paragraph', + content: 'fallback content', + metadata: { + bindings: { + content: { + source: 'core/post-meta', + args: { + key: 'movie_field', + }, + }, + }, + }, + }, + } ); + const paragraphBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Paragraph', + } ); + await expect( paragraphBlock ).toHaveText( + 'Movie field default value' + ); + await expect( paragraphBlock ).toHaveAttribute( + 'contenteditable', + 'true' + ); + await paragraphBlock.fill( 'new value' ); + // Check that the paragraph content attribute didn't change. + const [ paragraphBlockObject ] = await editor.getBlocks(); + expect( paragraphBlockObject.attributes.content ).toBe( + 'fallback content' + ); + // Check the value of the custom field is being updated by visiting the frontend. + const previewPage = await editor.openPreviewPage(); + await expect( + previewPage.locator( '#connected-paragraph' ) + ).toHaveText( 'new value' ); + } ); + test( 'should be possible to connect movie fields through the attributes panel', async ( { + editor, + page, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + } ); + await page.getByLabel( 'Attributes options' ).click(); + await page + .getByRole( 'menuitemcheckbox', { + name: 'Show content', + } ) + .click(); + await page + .getByRole( 'button', { + name: 'content', + } ) + .click(); + const movieField = page + .getByRole( 'menuitemradio' ) + .filter( { hasText: 'Movie field label' } ); + await expect( movieField ).toBeVisible(); + } ); + test( 'should not be possible to connect non-supported fields through the attributes panel', async ( { + editor, + page, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + } ); + await page.getByLabel( 'Attributes options' ).click(); + await page + .getByRole( 'menuitemcheckbox', { + name: 'Show content', + } ) + .click(); + await page + .getByRole( 'button', { + name: 'content', + } ) + .click(); + await expect( + page.getByRole( 'menuitemradio', { + name: 'String custom field', + } ) + ).toBeVisible(); + await expect( + page.getByRole( 'menuitemradio', { + name: 'Number custom field', + } ) + ).toBeHidden(); + await expect( + page.getByRole( 'menuitemradio', { + name: 'Integer custom field', + } ) + ).toBeHidden(); + await expect( + page.getByRole( 'menuitemradio', { + name: 'Boolean custom field', + } ) + ).toBeHidden(); + await expect( + page.getByRole( 'menuitemradio', { + name: 'Object custom field', + } ) + ).toBeHidden(); + await expect( + page.getByRole( 'menuitemradio', { + name: 'Array custom field', + } ) + ).toBeHidden(); + } ); + } ); +} ); diff --git a/test/e2e/specs/editor/various/block-deletion.spec.js b/test/e2e/specs/editor/various/block-deletion.spec.js index 00b51a94668d50..9346412c46bcb2 100644 --- a/test/e2e/specs/editor/various/block-deletion.spec.js +++ b/test/e2e/specs/editor/various/block-deletion.spec.js @@ -287,16 +287,15 @@ test.describe( 'Block deletion', () => { await expect.poll( editor.getBlocks ).toMatchObject( [ { name: 'core/paragraph', attributes: { content: 'First' } }, { name: 'core/paragraph', attributes: { content: 'Second' } }, + { name: 'core/paragraph', attributes: { content: '' } }, ] ); // Ensure that the newly created empty block is focused. - await expect.poll( editor.getBlocks ).toHaveLength( 2 ); + await expect.poll( editor.getBlocks ).toHaveLength( 3 ); await expect( - editor.canvas - .getByRole( 'document', { - name: 'Block: Paragraph', - } ) - .nth( 1 ) + editor.canvas.getByRole( 'document', { + name: 'Empty block', + } ) ).toBeFocused(); } ); diff --git a/test/e2e/specs/editor/various/copy-cut-paste.spec.js b/test/e2e/specs/editor/various/copy-cut-paste.spec.js index 33a65e6f5a1195..bca062b06416a1 100644 --- a/test/e2e/specs/editor/various/copy-cut-paste.spec.js +++ b/test/e2e/specs/editor/various/copy-cut-paste.spec.js @@ -565,6 +565,27 @@ test.describe( 'Copy/cut/paste', () => { ] ); } ); + test( 'should paste link to formatted text', async ( { + page, + pageUtils, + editor, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { content: 'test' }, + } ); + await page.keyboard.press( 'ArrowRight' ); + await page.keyboard.press( 'ArrowRight' ); + await pageUtils.pressKeys( 'shift+ArrowRight' ); + await pageUtils.pressKeys( 'shift+ArrowRight' ); + pageUtils.setClipboardData( { + plainText: 'https://wordpress.org/gutenberg', + html: 'https://wordpress.org/gutenberg', + } ); + await pageUtils.pressKeys( 'primary+v' ); + expect( await editor.getEditedPostContent() ).toMatchSnapshot(); + } ); + test( 'should auto-link', async ( { pageUtils, editor } ) => { await editor.insertBlock( { name: 'core/paragraph', diff --git a/test/e2e/specs/editor/various/inserting-blocks.spec.js b/test/e2e/specs/editor/various/inserting-blocks.spec.js index b4f23c8b8e2bbf..066937f097ba91 100644 --- a/test/e2e/specs/editor/various/inserting-blocks.spec.js +++ b/test/e2e/specs/editor/various/inserting-blocks.spec.js @@ -45,6 +45,10 @@ test.describe( 'Inserting blocks (@firefox, @webkit)', () => { { name: 'core/image' }, { name: 'core/paragraph' }, ] ); + + await expect( + editor.canvas.locator( '[data-type="core/paragraph"]' ) + ).toBeFocused(); } ); test( 'inserts blocks by dragging and dropping from the global inserter', async ( { diff --git a/test/e2e/specs/editor/various/parsing-patterns.spec.js b/test/e2e/specs/editor/various/parsing-patterns.spec.js index d8abe7a46fbc1b..3b801591aed5de 100644 --- a/test/e2e/specs/editor/various/parsing-patterns.spec.js +++ b/test/e2e/specs/editor/various/parsing-patterns.spec.js @@ -36,6 +36,14 @@ test.describe( 'Parsing patterns', () => { ], } ); } ); + + // Exit zoom out mode and select the inner buttons block to ensure + // the correct insertion point is selected. + await page.getByRole( 'button', { name: 'Zoom Out' } ).click(); + await editor.selectBlocks( + editor.canvas.locator( 'role=document[name="Block: Button"i]' ) + ); + await page.fill( 'role=region[name="Block Library"i] >> role=searchbox[name="Search for blocks and patterns"i]', 'whitespace' @@ -43,7 +51,7 @@ test.describe( 'Parsing patterns', () => { await page .locator( 'role=option[name="Pattern with top-level whitespace"i]' ) .click(); - expect( await editor.getBlocks() ).toMatchObject( [ + await expect.poll( editor.getBlocks ).toMatchObject( [ { name: 'core/buttons', innerBlocks: [ diff --git a/test/e2e/specs/editor/various/pattern-overrides.spec.js b/test/e2e/specs/editor/various/pattern-overrides.spec.js index 83f2f880f3bf1b..5fbd0e66b5fd02 100644 --- a/test/e2e/specs/editor/various/pattern-overrides.spec.js +++ b/test/e2e/specs/editor/various/pattern-overrides.spec.js @@ -150,7 +150,7 @@ test.describe( 'Pattern Overrides', () => { name: 'Block: Paragraph', } ); // Ensure the first pattern is selected. - await editor.selectBlocks( patternBlocks.first() ); + await patternBlocks.first().selectText(); await expect( paragraphs.first() ).not.toHaveAttribute( 'inert', 'true' @@ -168,7 +168,7 @@ test.describe( 'Pattern Overrides', () => { await page.keyboard.type( 'I would word it this way' ); // Ensure the second pattern is selected. - await editor.selectBlocks( patternBlocks.last() ); + await patternBlocks.last().selectText(); await patternBlocks .last() .getByRole( 'document', { diff --git a/test/e2e/specs/editor/various/publish-panel.spec.js b/test/e2e/specs/editor/various/publish-panel.spec.js index 534fea5289c9e1..1fe94ff334f3b2 100644 --- a/test/e2e/specs/editor/various/publish-panel.spec.js +++ b/test/e2e/specs/editor/various/publish-panel.spec.js @@ -58,7 +58,7 @@ test.describe( 'Post publish panel', () => { ).toBeFocused(); } ); - test( 'should move focus to the publish button in the panel', async ( { + test( 'should move focus to the cancel button in the panel', async ( { editor, page, } ) => { @@ -74,7 +74,7 @@ test.describe( 'Post publish panel', () => { page .getByRole( 'region', { name: 'Editor publish' } ) .locator( ':focus' ) - ).toHaveText( 'Publish' ); + ).toHaveText( 'Cancel' ); } ); test( 'should focus on the post list after publishing', async ( { diff --git a/test/e2e/specs/editor/various/splitting-merging.spec.js b/test/e2e/specs/editor/various/splitting-merging.spec.js index 29e7e5d64522c9..eba9f1d3163fd5 100644 --- a/test/e2e/specs/editor/various/splitting-merging.spec.js +++ b/test/e2e/specs/editor/various/splitting-merging.spec.js @@ -373,6 +373,103 @@ test.describe( 'splitting and merging blocks (@firefox, @webkit)', () => { ); } ); + // Fix for https://github.com/WordPress/gutenberg/issues/65174. + test( 'should handle unwrapping and merging blocks with empty contents', async ( { + editor, + page, + } ) => { + const emptyAlignedParagraph = { + name: 'core/paragraph', + attributes: { content: '', align: 'center', dropCap: false }, + innerBlocks: [], + }; + const emptyAlignedHeading = { + name: 'core/heading', + attributes: { content: '', textAlign: 'center', level: 2 }, + innerBlocks: [], + }; + const headingWithContent = { + name: 'core/heading', + attributes: { content: 'heading', level: 2 }, + innerBlocks: [], + }; + const placeholderBlock = { name: 'core/separator' }; + await editor.insertBlock( { + name: 'core/group', + innerBlocks: [ + emptyAlignedParagraph, + emptyAlignedHeading, + headingWithContent, + placeholderBlock, + ], + } ); + await editor.canvas + .getByRole( 'document', { name: 'Empty block' } ) + .focus(); + + await page.keyboard.press( 'Backspace' ); + await expect + .poll( editor.getBlocks, 'Remove the default empty block' ) + .toEqual( [ + { + name: 'core/group', + attributes: { tagName: 'div' }, + innerBlocks: [ + emptyAlignedHeading, + headingWithContent, + expect.objectContaining( placeholderBlock ), + ], + }, + ] ); + + // Move the caret to the beginning of the empty heading block. + await page.keyboard.press( 'ArrowDown' ); + await page.keyboard.press( 'Backspace' ); + await expect + .poll( + editor.getBlocks, + 'Convert the non-default empty block to a default block' + ) + .toEqual( [ + { + name: 'core/group', + attributes: { tagName: 'div' }, + innerBlocks: [ + emptyAlignedParagraph, + headingWithContent, + expect.objectContaining( placeholderBlock ), + ], + }, + ] ); + await page.keyboard.press( 'Backspace' ); + await expect.poll( editor.getBlocks ).toEqual( [ + { + name: 'core/group', + attributes: { tagName: 'div' }, + innerBlocks: [ + headingWithContent, + expect.objectContaining( placeholderBlock ), + ], + }, + ] ); + + // Move the caret to the beginning of the "heading" heading block. + await page.keyboard.press( 'ArrowDown' ); + await page.keyboard.press( 'Backspace' ); + await expect + .poll( editor.getBlocks, 'Lift the non-empty non-default block' ) + .toEqual( [ + headingWithContent, + { + name: 'core/group', + attributes: { tagName: 'div' }, + innerBlocks: [ + expect.objectContaining( placeholderBlock ), + ], + }, + ] ); + } ); + test.describe( 'test restore selection when merge produces more than one block', () => { const snap1 = [ { diff --git a/test/e2e/specs/interactivity/directive-class.spec.ts b/test/e2e/specs/interactivity/directive-class.spec.ts index b7e085aba15143..96b725568767ae 100644 --- a/test/e2e/specs/interactivity/directive-class.spec.ts +++ b/test/e2e/specs/interactivity/directive-class.spec.ts @@ -108,4 +108,14 @@ test.describe( 'data-wp-class', () => { const el = page.getByTestId( 'can use classes with several dashes' ); await expect( el ).toHaveClass( 'main-bg----color' ); } ); + + test( 'can use "default" as a class name', async ( { page } ) => { + const el = page.getByTestId( 'class name default' ); + const btn = page.getByTestId( 'toggle class name default' ); + await expect( el ).not.toHaveClass( 'default' ); + await btn.click(); + await expect( el ).toHaveClass( 'default' ); + await btn.click(); + await expect( el ).not.toHaveClass( 'default' ); + } ); } ); diff --git a/test/e2e/specs/interactivity/get-sever-context.spec.ts b/test/e2e/specs/interactivity/get-sever-context.spec.ts new file mode 100644 index 00000000000000..d7bc4075f97604 --- /dev/null +++ b/test/e2e/specs/interactivity/get-sever-context.spec.ts @@ -0,0 +1,166 @@ +/** + * Internal dependencies + */ +import { test, expect } from './fixtures'; + +test.describe( 'getServerContext()', () => { + test.beforeAll( async ( { interactivityUtils: utils } ) => { + const parent = { + prop: 'parent', + nested: { + prop: 'parent', + }, + inherited: { + prop: 'parent', + }, + }; + + const parentModified = { + prop: 'parentModified', + nested: { + prop: 'parentModified', + }, + inherited: { + prop: 'parentModified', + }, + }; + + const parentNewProps = { + prop: 'parent', + newProp: 'parent', + nested: { + prop: 'parent', + newProp: 'parent', + }, + inherited: { + prop: 'parent', + newProp: 'parent', + }, + }; + + const child = { + prop: 'child', + nested: { + prop: 'child', + }, + }; + + const childModified = { + prop: 'childModified', + nested: { + prop: 'childModified', + }, + }; + + const childNewProps = { + prop: 'child', + newProp: 'child', + nested: { + prop: 'child', + newProp: 'child', + }, + }; + + await utils.activatePlugins(); + const link1 = await utils.addPostWithBlock( 'test/get-server-context', { + alias: 'getServerContext() - modified', + attributes: { + parentContext: parentModified, + childContext: childModified, + }, + } ); + const link2 = await utils.addPostWithBlock( 'test/get-server-context', { + alias: 'getServerContext() - new props', + attributes: { + parentContext: parentNewProps, + childContext: childNewProps, + }, + } ); + await utils.addPostWithBlock( 'test/get-server-context', { + alias: 'getServerContext() - main', + attributes: { + links: { modified: link1, newProps: link2 }, + parentContext: parent, + childContext: child, + }, + } ); + } ); + + test.beforeEach( async ( { interactivityUtils: utils, page } ) => { + await page.goto( utils.getLink( 'getServerContext() - main' ) ); + } ); + + test.afterAll( async ( { interactivityUtils: utils } ) => { + await utils.deactivatePlugins(); + await utils.deleteAllPosts(); + } ); + + test( 'should update modified props on navigation', async ( { page } ) => { + const prop = page.getByTestId( 'prop' ); + const nestedProp = page.getByTestId( 'nested.prop' ); + const inheritedProp = page.getByTestId( 'inherited.prop' ); + + await expect( prop ).toHaveText( 'child' ); + await expect( nestedProp ).toHaveText( 'child' ); + await expect( inheritedProp ).toHaveText( 'parent' ); + + await page.getByTestId( 'modified' ).click(); + + await expect( prop ).toHaveText( 'childModified' ); + await expect( nestedProp ).toHaveText( 'childModified' ); + await expect( inheritedProp ).toHaveText( 'parentModified' ); + + await page.goBack(); + + await expect( prop ).toHaveText( 'child' ); + await expect( nestedProp ).toHaveText( 'child' ); + await expect( inheritedProp ).toHaveText( 'parent' ); + } ); + + test( 'should add new props on navigation', async ( { page } ) => { + const newProp = page.getByTestId( 'newProp' ); + const nestedNewProp = page.getByTestId( 'nested.newProp' ); + const inheritedNewProp = page.getByTestId( 'inherited.newProp' ); + + await expect( newProp ).toBeEmpty(); + await expect( nestedNewProp ).toBeEmpty(); + await expect( inheritedNewProp ).toBeEmpty(); + + await page.getByTestId( 'newProps' ).click(); + + await expect( newProp ).toHaveText( 'child' ); + await expect( nestedNewProp ).toHaveText( 'child' ); + await expect( inheritedNewProp ).toHaveText( 'parent' ); + } ); + + test( 'should keep new props on navigation', async ( { page } ) => { + const newProp = page.getByTestId( 'newProp' ); + const nestedNewProp = page.getByTestId( 'nested.newProp' ); + const inheritedNewProp = page.getByTestId( 'inherited.newProp' ); + + await page.getByTestId( 'newProps' ).click(); + + await expect( newProp ).toHaveText( 'child' ); + await expect( nestedNewProp ).toHaveText( 'child' ); + await expect( inheritedNewProp ).toHaveText( 'parent' ); + + await page.goBack(); + + await expect( newProp ).toHaveText( 'child' ); + await expect( nestedNewProp ).toHaveText( 'child' ); + await expect( inheritedNewProp ).toHaveText( 'parent' ); + } ); + + test( 'should prevent any manual modifications', async ( { page } ) => { + const prop = page.getByTestId( 'prop' ); + const button = page.getByTestId( 'tryToModifyServerContext' ); + + await expect( prop ).toHaveText( 'child' ); + await expect( button ).toHaveText( 'modify' ); + + await button.click(); + + await expect( prop ).toHaveText( 'child' ); + await expect( button ).toHaveText( 'not modified ✅' ); + } ); +} ); diff --git a/test/e2e/specs/interactivity/get-sever-state.spec.ts b/test/e2e/specs/interactivity/get-sever-state.spec.ts new file mode 100644 index 00000000000000..16406c1d824463 --- /dev/null +++ b/test/e2e/specs/interactivity/get-sever-state.spec.ts @@ -0,0 +1,119 @@ +/** + * Internal dependencies + */ +import { test, expect } from './fixtures'; + +test.describe( 'getServerState()', () => { + test.beforeAll( async ( { interactivityUtils: utils } ) => { + await utils.activatePlugins(); + const link1 = await utils.addPostWithBlock( 'test/get-server-state', { + alias: 'getServerState() - link 1', + attributes: { + state: { + prop: 'link 1', + newProp: 'link 1', + nested: { + prop: 'link 1', + newProp: 'link 1', + }, + }, + }, + } ); + const link2 = await utils.addPostWithBlock( 'test/get-server-state', { + alias: 'getServerState() - link 2', + attributes: { + state: { + prop: 'link 2', + newProp: 'link 2', + nested: { + prop: 'link 2', + newProp: 'link 2', + }, + }, + }, + } ); + await utils.addPostWithBlock( 'test/get-server-state', { + alias: 'getServerState() - main', + attributes: { + title: 'Main', + links: [ link1, link2 ], + state: { + prop: 'main', + nested: { + prop: 'main', + }, + }, + }, + } ); + } ); + + test.beforeEach( async ( { interactivityUtils: utils, page } ) => { + await page.goto( utils.getLink( 'getServerState() - main' ) ); + } ); + + test.afterAll( async ( { interactivityUtils: utils } ) => { + await utils.deactivatePlugins(); + await utils.deleteAllPosts(); + } ); + + test( 'should update existing state props on navigation', async ( { + page, + } ) => { + const prop = page.getByTestId( 'prop' ); + const nestedProp = page.getByTestId( 'nested.prop' ); + + await expect( prop ).toHaveText( 'main' ); + await expect( nestedProp ).toHaveText( 'main' ); + + await page.getByTestId( 'link 1' ).click(); + + await expect( prop ).toHaveText( 'link 1' ); + await expect( nestedProp ).toHaveText( 'link 1' ); + + await page.goBack(); + await expect( prop ).toHaveText( 'main' ); + await expect( nestedProp ).toHaveText( 'main' ); + + await page.getByTestId( 'link 2' ).click(); + + await expect( prop ).toHaveText( 'link 2' ); + await expect( nestedProp ).toHaveText( 'link 2' ); + } ); + + test( 'should add new state props and keep them on navigation', async ( { + page, + } ) => { + const newProp = page.getByTestId( 'newProp' ); + const nestedNewProp = page.getByTestId( 'nested.newProp' ); + + await expect( newProp ).toBeEmpty(); + await expect( nestedNewProp ).toBeEmpty(); + + await page.getByTestId( 'link 1' ).click(); + + await expect( newProp ).toHaveText( 'link 1' ); + await expect( nestedNewProp ).toHaveText( 'link 1' ); + + await page.goBack(); + await expect( newProp ).toHaveText( 'link 1' ); + await expect( nestedNewProp ).toHaveText( 'link 1' ); + + await page.getByTestId( 'link 2' ).click(); + + await expect( newProp ).toHaveText( 'link 2' ); + await expect( nestedNewProp ).toHaveText( 'link 2' ); + } ); + + test( 'should prevent any manual modifications', async ( { page } ) => { + const prop = page.getByTestId( 'prop' ); + const button = page.getByTestId( 'tryToModifyServerState' ); + + await expect( prop ).toHaveText( 'main' ); + await expect( button ).toHaveText( 'modify' ); + + await button.click(); + + await expect( prop ).toHaveText( 'main' ); + await expect( button ).toHaveText( 'not modified ✅' ); + } ); +} ); diff --git a/test/e2e/specs/site-editor/command-center.spec.js b/test/e2e/specs/site-editor/command-center.spec.js index 5b049cda252a8b..19318081aa171b 100644 --- a/test/e2e/specs/site-editor/command-center.spec.js +++ b/test/e2e/specs/site-editor/command-center.spec.js @@ -28,10 +28,12 @@ test.describe( 'Site editor command palette', () => { await page.keyboard.type( 'new page' ); await page.getByRole( 'option', { name: 'Add new page' } ).click(); await expect( page ).toHaveURL( - '/wp-admin/post-new.php?post_type=page' + /\/wp-admin\/site-editor.php\?postId=(\d+)&postType=page&canvas=edit/ ); await expect( - editor.canvas.getByRole( 'textbox', { name: 'Add title' } ) + editor.canvas + .getByLabel( 'Block: Title' ) + .locator( '[data-rich-text-placeholder="No title"]' ) ).toBeVisible(); } ); diff --git a/test/e2e/specs/site-editor/font-library.spec.js b/test/e2e/specs/site-editor/font-library.spec.js index 6d699f4b02a63e..7271768206d1b6 100644 --- a/test/e2e/specs/site-editor/font-library.spec.js +++ b/test/e2e/specs/site-editor/font-library.spec.js @@ -38,7 +38,7 @@ test.describe( 'Font Library', () => { ).toBeVisible(); } ); - test( 'should display the "Add fonts" button', async ( { page } ) => { + test( 'should display the "Manage fonts" icon', async ( { page } ) => { await page .getByRole( 'region', { name: 'Editor top bar' } ) .getByRole( 'button', { name: 'Styles' } ) @@ -46,10 +46,10 @@ test.describe( 'Font Library', () => { await page .getByRole( 'button', { name: 'Typography Styles' } ) .click(); - const addFontsButton = page.getByRole( 'button', { - name: 'Add fonts', + const manageFontsIcon = page.getByRole( 'button', { + name: 'Manage fonts', } ); - await expect( addFontsButton ).toBeVisible(); + await expect( manageFontsIcon ).toBeVisible(); } ); } ); @@ -66,9 +66,7 @@ test.describe( 'Font Library', () => { } ); } ); - test( 'should display the "Manage fonts" button', async ( { - page, - } ) => { + test( 'should display the "Manage fonts" icon', async ( { page } ) => { await page .getByRole( 'region', { name: 'Editor top bar' } ) .getByRole( 'button', { name: 'Styles' } ) @@ -76,13 +74,13 @@ test.describe( 'Font Library', () => { await page .getByRole( 'button', { name: 'Typography Styles' } ) .click(); - const manageFontsButton = page.getByRole( 'button', { + const manageFontsIcon = page.getByRole( 'button', { name: 'Manage fonts', } ); - await expect( manageFontsButton ).toBeVisible(); + await expect( manageFontsIcon ).toBeVisible(); } ); - test( 'should open the "Manage fonts" modal when clicking the "Manage fonts" button', async ( { + test( 'should open the "Manage fonts" modal when clicking the "Manage fonts" icon', async ( { page, } ) => { await page diff --git a/test/gutenberg-test-themes/block-bindings/index.php b/test/gutenberg-test-themes/block-bindings/index.php new file mode 100644 index 00000000000000..0c6530acc1aaff --- /dev/null +++ b/test/gutenberg-test-themes/block-bindings/index.php @@ -0,0 +1,9 @@ + +

Custom template

+ diff --git a/test/gutenberg-test-themes/block-bindings/templates/index.html b/test/gutenberg-test-themes/block-bindings/templates/index.html new file mode 100644 index 00000000000000..ab136ac8df9a7d --- /dev/null +++ b/test/gutenberg-test-themes/block-bindings/templates/index.html @@ -0,0 +1,8 @@ + +
+ + + + +
+ diff --git a/test/gutenberg-test-themes/block-bindings/templates/single-movie.html b/test/gutenberg-test-themes/block-bindings/templates/single-movie.html new file mode 100644 index 00000000000000..cd05d5fe917fea --- /dev/null +++ b/test/gutenberg-test-themes/block-bindings/templates/single-movie.html @@ -0,0 +1,2 @@ + + diff --git a/test/gutenberg-test-themes/block-bindings/templates/single.html b/test/gutenberg-test-themes/block-bindings/templates/single.html new file mode 100644 index 00000000000000..cd05d5fe917fea --- /dev/null +++ b/test/gutenberg-test-themes/block-bindings/templates/single.html @@ -0,0 +1,2 @@ + + diff --git a/test/gutenberg-test-themes/block-bindings/theme.json b/test/gutenberg-test-themes/block-bindings/theme.json new file mode 100644 index 00000000000000..c996b014328398 --- /dev/null +++ b/test/gutenberg-test-themes/block-bindings/theme.json @@ -0,0 +1,18 @@ +{ + "$schema": "../../../schemas/json/theme.json", + "version": 3, + "settings": { + "appearanceTools": true, + "layout": { + "contentSize": "840px", + "wideSize": "1100px" + } + }, + "customTemplates": [ + { + "name": "custom-template", + "title": "Custom", + "postTypes": [ "post", "movie" ] + } + ] +} diff --git a/tools/webpack/script-modules.js b/tools/webpack/script-modules.js index 18287c96d83c8a..021f11f5f5ed95 100644 --- a/tools/webpack/script-modules.js +++ b/tools/webpack/script-modules.js @@ -89,11 +89,11 @@ module.exports = { }, output: { devtoolNamespace: 'wp', - filename: './build-module/[name].min.js', + filename: '[name].min.js', library: { type: 'module', }, - path: join( __dirname, '..', '..' ), + path: join( __dirname, '..', '..', 'build-module' ), environment: { module: true }, module: true, chunkFormat: 'module', @@ -102,7 +102,13 @@ module.exports = { resolve: { extensions: [ '.js', '.ts', '.tsx' ], }, - plugins: [ ...plugins, new DependencyExtractionWebpackPlugin() ], + plugins: [ + ...plugins, + new DependencyExtractionWebpackPlugin( { + combineAssets: true, + combinedOutputFile: `./assets.php`, + } ), + ], watchOptions: { ignored: [ '**/node_modules' ], aggregateTimeout: 500, diff --git a/tools/webpack/shared.js b/tools/webpack/shared.js index c8c5b05c7d151b..f30d3a830f3eb1 100644 --- a/tools/webpack/shared.js +++ b/tools/webpack/shared.js @@ -25,7 +25,7 @@ const baseConfig = { parallel: true, terserOptions: { output: { - comments: /(translators:|wp:polyfill)/i, + comments: /translators:/i, }, compress: { passes: 2,