diff --git a/bin/plugin/commands/performance.js b/bin/plugin/commands/performance.js index 31a537f373731..81aa39820c8b5 100644 --- a/bin/plugin/commands/performance.js +++ b/bin/plugin/commands/performance.js @@ -37,6 +37,7 @@ const config = require( '../config' ); * @property {number[]} firstContentfulPaint Represents the time when the browser first renders any text or media. * @property {number[]} firstBlock Represents the time when Puppeteer first sees a block selector in the DOM. * @property {number[]} type Average type time. + * @property {number[]} typeContainer Average type time within a container. * @property {number[]} focus Average block selection time. * @property {number[]} inserterOpen Average time to open global inserter. * @property {number[]} inserterSearch Average time to search the inserter. @@ -56,6 +57,9 @@ const config = require( '../config' ); * @property {number=} type Average type time. * @property {number=} minType Minimum type time. * @property {number=} maxType Maximum type time. + * @property {number=} typeContainer Average type time within a container. + * @property {number=} minTypeContainer Minimum type time within a container. + * @property {number=} maxTypeContainer Maximum type time within a container. * @property {number=} focus Average block selection time. * @property {number=} minFocus Min block selection time. * @property {number=} maxFocus Max block selection time. @@ -129,6 +133,9 @@ function curateResults( results ) { type: average( results.type ), minType: Math.min( ...results.type ), maxType: Math.max( ...results.type ), + typeContainer: average( results.typeContainer ), + minTypeContainer: Math.min( ...results.typeContainer ), + maxTypeContainer: Math.max( ...results.typeContainer ), focus: average( results.focus ), minFocus: Math.min( ...results.focus ), maxFocus: Math.max( ...results.focus ), @@ -393,6 +400,15 @@ async function runPerformanceTests( branches, options ) { type: rawResults.map( ( r ) => r[ branch ].type ), minType: rawResults.map( ( r ) => r[ branch ].minType ), maxType: rawResults.map( ( r ) => r[ branch ].maxType ), + typeContainer: rawResults.map( + ( r ) => r[ branch ].typeContainer + ), + minTypeContainer: rawResults.map( + ( r ) => r[ branch ].minTypeContainer + ), + maxTypeContainer: rawResults.map( + ( r ) => r[ branch ].maxTypeContainer + ), focus: rawResults.map( ( r ) => r[ branch ].focus ), minFocus: rawResults.map( ( r ) => r[ branch ].minFocus ), maxFocus: rawResults.map( ( r ) => r[ branch ].maxFocus ), diff --git a/changelog.txt b/changelog.txt index 5427245e8d468..8774c72580adf 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,327 @@ == Changelog == += 14.8.0-rc.1 = + +## Changelog + +### Enhancements + +#### Block Library +- Add a current-menu-ancestor class to navigation items. ([40778](https://github.com/WordPress/gutenberg/pull/40778)) +- Page List Block: Adds a longdash tree to the parent selector. ([46336](https://github.com/WordPress/gutenberg/pull/46336)) +- Page List Block: Hide page list edit button if no pages are available. ([46331](https://github.com/WordPress/gutenberg/pull/46331)) +- Reusable block: Pluralize the message "Convert to regular blocks" depending on the number of blocks contained. ([45819](https://github.com/WordPress/gutenberg/pull/45819)) +- Add page list to Link UI transforms in Nav block. ([46426](https://github.com/WordPress/gutenberg/pull/46426)) +- Heading Block: Don't rely on the experimental selector anymore. ([46284](https://github.com/WordPress/gutenberg/pull/46284)) +- Media & Text Block: Create undo history when media width is changed. ([46084](https://github.com/WordPress/gutenberg/pull/46084)) +- Navigation block: Add location->primary to fallback nav creation for classic menus. ([45976](https://github.com/WordPress/gutenberg/pull/45976)) +- Navigation block: Update fallback nav creation to the most recently created menu. ([46286](https://github.com/WordPress/gutenberg/pull/46286)) +- Navigation: Add a 'open list view' button. ([46335](https://github.com/WordPress/gutenberg/pull/46335)) +- Navigation: Removes the header from the navigation list view in the experiment. ([46070](https://github.com/WordPress/gutenberg/pull/46070)) +- Page List: Add convert panel to Inspector Controls when within Nav block. ([46352](https://github.com/WordPress/gutenberg/pull/46352)) +- Page List: Prevent users from adding inner blocks to Page List. ([46269](https://github.com/WordPress/gutenberg/pull/46269)) +- Query: Remove color block supports. ([46147](https://github.com/WordPress/gutenberg/pull/46147)) +- Table block: Make `figcaption` styles consistent between editor and front end. ([46172](https://github.com/WordPress/gutenberg/pull/46172)) +- List/quote: Unwrap inner block when pressing Backspace at start. ([45075](https://github.com/WordPress/gutenberg/pull/45075)) + +#### Inspector Controls +- Sidebar Tabs: Refine the use of inspector tabs and disable filters for Nav blocks. ([46346](https://github.com/WordPress/gutenberg/pull/46346)) +- Sidebar Tabs: Use editor settings to override display. ([46321](https://github.com/WordPress/gutenberg/pull/46321)) +- Summary panel: Try improving spacing and grid. ([46267](https://github.com/WordPress/gutenberg/pull/46267)) + +#### Global Styles +- Add Style Book to Global Styles. ([45960](https://github.com/WordPress/gutenberg/pull/45960)) +- Add block preview component in global styles. ([45719](https://github.com/WordPress/gutenberg/pull/45719)) +- Move border from layout to own menu. ([45995](https://github.com/WordPress/gutenberg/pull/45995)) +- Add a css style to theme.json to allow setting of custom css strings. ([46255](https://github.com/WordPress/gutenberg/pull/46255)) +- Expose before filter hook in useSettings for injecting block settings in the editor. ([45089](https://github.com/WordPress/gutenberg/pull/45089)) +- Global styles: Add custom CSS panel to site editor. ([46141](https://github.com/WordPress/gutenberg/pull/46141)) + +#### Site Editor +- Allow adding new templates and template parts directly from the sidebar. ([46458](https://github.com/WordPress/gutenberg/pull/46458)) +- Synchronize the sidebar state in the URL. ([46433](https://github.com/WordPress/gutenberg/pull/46433)) +- Try template drill down on the shell sidebar (browse mode). ([45100](https://github.com/WordPress/gutenberg/pull/45100)) + +#### Block Editor +- Update the synced block hover styles in Inserter. ([46442](https://github.com/WordPress/gutenberg/pull/46442)) +- Add new selector getLastInsertedBlockClientId. ([46531](https://github.com/WordPress/gutenberg/pull/46531)) +- Block editor: Hide fixed contextual toolbar. ([46298](https://github.com/WordPress/gutenberg/pull/46298)) +- Inserter: Pattern title tooltip. ([46419](https://github.com/WordPress/gutenberg/pull/46419)) +- useNestedSettingsUpdate: Prevent unneeded syncing of falsy templateLock values. ([46357](https://github.com/WordPress/gutenberg/pull/46357)) +- Design: Augmented shadows for modals and popovers. ([46228](https://github.com/WordPress/gutenberg/pull/46228)) + +#### Components +- Tabs: Try a simpler tab focus style, alt. ([46276](https://github.com/WordPress/gutenberg/pull/46276)) +- BaseControl: Add convenience hook to generate id-related props. ([46170](https://github.com/WordPress/gutenberg/pull/46170)) +- Dashicon: Refactor to TypeScript. ([45924](https://github.com/WordPress/gutenberg/pull/45924)) +- Lighten borders to gray-600. ([46252](https://github.com/WordPress/gutenberg/pull/46252)) +- Popover: Check positioning by adding and testing is-positioned class. ([46429](https://github.com/WordPress/gutenberg/pull/46429)) + +### Icons +- Icons: Update the border icon. ([46264](https://github.com/WordPress/gutenberg/pull/46264)) + +#### Testing +- Tests: Fix `toBePositionedPopover` matcher message function. ([46239](https://github.com/WordPress/gutenberg/pull/46239)) + +#### Accessibility +- Reorganize the site editor to introduce Browse Mode. ([44770](https://github.com/WordPress/gutenberg/pull/44770)) + +#### Plugin +- Update the Gutenberg plugin to require at least the WP 6.0 version. ([46102](https://github.com/WordPress/gutenberg/pull/46102)) +- PHP: Backport changes from core theme resolver. ([46250](https://github.com/WordPress/gutenberg/pull/46250)) +- Update: Move gutenberg_register_core_block_patterns from 6.1 to 6.2. ([46249](https://github.com/WordPress/gutenberg/pull/46249)) +- Upgrade React packages to v18. ([45235](https://github.com/WordPress/gutenberg/pull/45235)) + +#### Themes +- Empty Theme: Add the `$schema` property in `theme.json` and rename template directories. ([46300](https://github.com/WordPress/gutenberg/pull/46300)) + +#### Mobile +- Mobile: Disable Unsupported Block Editor Tests (Android). ([46542](https://github.com/WordPress/gutenberg/pull/46542)) +- Mobile: Inserter - Remove `.done()` usage. ([46460](https://github.com/WordPress/gutenberg/pull/46460)) +- Mobile: Update Heading block end-to-end test. ([46220](https://github.com/WordPress/gutenberg/pull/46220)) +- Mobile: Updates packages to not use Git HTTPS URLs. ([46422](https://github.com/WordPress/gutenberg/pull/46422)) + +### Bug Fixes + +#### Block Library +- Fix Nav Submenu block Link UI text control. ([46243](https://github.com/WordPress/gutenberg/pull/46243)) +- Fix auto Nav menu creation due to page list inner blocks. ([46223](https://github.com/WordPress/gutenberg/pull/46223)) +- Handle innerContent too when removing innerBlocks. ([46377](https://github.com/WordPress/gutenberg/pull/46377)) +- Image Block: Ensure drag handle matches cursor position when resizing a center aligned image. ([46497](https://github.com/WordPress/gutenberg/pull/46497)) +- Navigation Block: Add social link singular to list of blocks to be allowed. ([46374](https://github.com/WordPress/gutenberg/pull/46374)) +- Navigation Block: Fixes adding a submenu. ([46364](https://github.com/WordPress/gutenberg/pull/46364)) +- Navigation Block: Prevent circular references in navigation block rendering. ([46387](https://github.com/WordPress/gutenberg/pull/46387)) +- Navigation Block: Recursively remove Navigation block’s from appearing inside Navigation block on front of site. ([46279](https://github.com/WordPress/gutenberg/pull/46279)) +- Navigation link: Use stripHTML. ([46317](https://github.com/WordPress/gutenberg/pull/46317)) +- Page List Block: Fix error loading page list parent options. ([46327](https://github.com/WordPress/gutenberg/pull/46327)) +- Query Loop Block: Add migration of colors to v2 deprecation. ([46522](https://github.com/WordPress/gutenberg/pull/46522)) +- Site Logo: Correctly set the image's natural height and width. ([46214](https://github.com/WordPress/gutenberg/pull/46214)) +- Strip markup from link label data in inspector. ([46171](https://github.com/WordPress/gutenberg/pull/46171)) +- Template Parts: Fix modal search stacking context. ([46421](https://github.com/WordPress/gutenberg/pull/46421)) +- Video: Avoid an error when removal is locked. ([46324](https://github.com/WordPress/gutenberg/pull/46324)) +- Layout child fixed size should not be fixed by default and should always have a value set. ([46139](https://github.com/WordPress/gutenberg/pull/46139)) + +#### Blocks +- Paste handler: Remove styles on inline paste. ([46402](https://github.com/WordPress/gutenberg/pull/46402)) +- Improve performance of gutenberg_render_layout_support_flag. ([46074](https://github.com/WordPress/gutenberg/pull/46074)) + +#### Global Styles +- Allow indirect properties when unfiltered_html is not allowed. ([46388](https://github.com/WordPress/gutenberg/pull/46388)) +- Fix Reset to defaults action by moving fills to be within context provider. ([46486](https://github.com/WordPress/gutenberg/pull/46486)) +- Fix duplication of synced block colors in CSS output. ([46297](https://github.com/WordPress/gutenberg/pull/46297)) +- Make style book label font size 11px. ([46341](https://github.com/WordPress/gutenberg/pull/46341)) +- Style Book: Clear Global Styles navigation history when selecting a block. ([46391](https://github.com/WordPress/gutenberg/pull/46391)) + +#### Block Editor +- Block Editor: Fix content locked patterns. ([46494](https://github.com/WordPress/gutenberg/pull/46494)) +- Block Editor: Fix memoized pattern selector dependant arguments. ([46238](https://github.com/WordPress/gutenberg/pull/46238)) +- Block Editor: Restore draggable chip styles. ([46396](https://github.com/WordPress/gutenberg/pull/46396)) +- Block Editor: Revert deoptimization useNestedSettingsUpdate. ([46350](https://github.com/WordPress/gutenberg/pull/46350)) +- Block Editor: Fix some usages of useSelect that return unstable results. ([46226](https://github.com/WordPress/gutenberg/pull/46226)) +- useInnerBlockTemplateSync: Cancel template sync on innerBlocks change or unmount. ([46307](https://github.com/WordPress/gutenberg/pull/46307)) + +#### Patterns +- Add new pattern categories. ([46144](https://github.com/WordPress/gutenberg/pull/46144)) +- Block Editor: Add initial view mode in `BlockPatternSetup`. ([46399](https://github.com/WordPress/gutenberg/pull/46399)) + +#### Site Editor +- Do not remount iframe. ([46431](https://github.com/WordPress/gutenberg/pull/46431)) +- Fix the top bar 'exit' animation. ([46533](https://github.com/WordPress/gutenberg/pull/46533)) +- Keep edited entity in sync when Editor canvas isn't mounted. ([46524](https://github.com/WordPress/gutenberg/pull/46524)) +- [Site Editor]: Add default white background for themes with no `background color` set. ([46314](https://github.com/WordPress/gutenberg/pull/46314)) + +#### Components +- InputControl: Fix `Flex` wrapper usage. ([46213](https://github.com/WordPress/gutenberg/pull/46213)) +- Modal: Fix unexpected modal closing in IME Composition. ([46453](https://github.com/WordPress/gutenberg/pull/46453)) +- MaybeCategoryPanel: Avoid 403 requests for users with low permissions. ([46349](https://github.com/WordPress/gutenberg/pull/46349)) +- Rich text: Add button to clear unknown format. ([44086](https://github.com/WordPress/gutenberg/pull/44086)) + +#### Document Settings +- Fix template title in `summary` panel and requests for low privileged users. ([46304](https://github.com/WordPress/gutenberg/pull/46304)) +- Permalink: Hide edit field for users without publishing capabilities. ([46361](https://github.com/WordPress/gutenberg/pull/46361)) + +#### Patterns +- Content lock: Make filter hook namespace unique. ([46344](https://github.com/WordPress/gutenberg/pull/46344)) + +#### Layout +- Child Layout controls: Fix help text for height. ([46319](https://github.com/WordPress/gutenberg/pull/46319)) + +#### Widgets Editor +- Shortcuts: Add Ctrl+Y for redo to all editor instances on Windows. ([43392](https://github.com/WordPress/gutenberg/pull/43392)) + +#### Block API +- HTML block: Fix parsing. ([27268](https://github.com/WordPress/gutenberg/pull/27268)) + +#### Mobile +- Social Links mobile test: Wait for URL bottom sheet to appear. ([46308](https://github.com/WordPress/gutenberg/pull/46308)) + +### Performance + +#### Components +- Avoid paint on popover when hovering content. ([46201](https://github.com/WordPress/gutenberg/pull/46201)) +- CircularOption: Avoid paint on circular option hover. ([46197](https://github.com/WordPress/gutenberg/pull/46197)) +- Lodash: Replace `_.isEqual()` with `fastDeepEqual`. ([46200](https://github.com/WordPress/gutenberg/pull/46200)) +- Popover: Avoid paint on popovers when scrolling. ([46187](https://github.com/WordPress/gutenberg/pull/46187)) +- Resizable Box: Avoid paint on resizable-box handles. ([46196](https://github.com/WordPress/gutenberg/pull/46196)) +- ListView: Avoid paint on list view item hover. ([46188](https://github.com/WordPress/gutenberg/pull/46188)) + +#### Code Quality +- Lodash: Refactor `blocks` away from `_.find()`. ([46428](https://github.com/WordPress/gutenberg/pull/46428)) +- Lodash: Refactor `core-data` away from `_.find()`. ([46468](https://github.com/WordPress/gutenberg/pull/46468)) +- Lodash: Refactor `edit-site` away from `_.find()`. ([46539](https://github.com/WordPress/gutenberg/pull/46539)) +- Lodash: Refactor away from `_.orderBy()`. ([45146](https://github.com/WordPress/gutenberg/pull/45146)) +- Lodash: Refactor block library away from `_.find()`. ([46430](https://github.com/WordPress/gutenberg/pull/46430)) +- Remove usage of get_default_block_editor_settings. ([46112](https://github.com/WordPress/gutenberg/pull/46112)) + +#### Post Editor +- Lodash: Refactor editor away from `_.find()`. ([46464](https://github.com/WordPress/gutenberg/pull/46464)) +- Lodash: Refactor post editor away from `_.find()`. ([46432](https://github.com/WordPress/gutenberg/pull/46432)) + +#### Block Editor +- Avoid paint on inserter animation. ([46185](https://github.com/WordPress/gutenberg/pull/46185)) +- Improve inserter search performance. ([46153](https://github.com/WordPress/gutenberg/pull/46153)) +- Block Editor: Refactor the "order" state in the block editor reducer to use a map instead of a plain object. ([46221](https://github.com/WordPress/gutenberg/pull/46221)) +- Block Editor: Refactor the block-editor parents state to use maps instead of objects. ([46225](https://github.com/WordPress/gutenberg/pull/46225)) +- Refactor the block-editor "tree" state to use maps instead of objects. ([46229](https://github.com/WordPress/gutenberg/pull/46229)) +- Refactor the block-editor byClientId redux state to use maps instead of plain objects. ([46204](https://github.com/WordPress/gutenberg/pull/46204)) +- Fix typing performance issue for container blocks. ([46527](https://github.com/WordPress/gutenberg/pull/46527)) + +#### Testing +- E2E: Fix performance tests by making inserter search container waiting optional. ([46268](https://github.com/WordPress/gutenberg/pull/46268)) + +#### Mobile +- Columns mobile block: Avoid returning unstable innerWidths from useSelect. ([46403](https://github.com/WordPress/gutenberg/pull/46403)) + +### Experiments + +#### Block Library +- Navigation List View: Remove empty cell when there is no edit button. ([46439](https://github.com/WordPress/gutenberg/pull/46439)) + +#### Web Fonts +- WP Webfonts: Avoid duplicated font families if the font family name was defined using fallback values. ([46378](https://github.com/WordPress/gutenberg/pull/46378)) + +### Documentation +- Adds clarifications and clears up inaccuracies. ([46283](https://github.com/WordPress/gutenberg/pull/46283)) +- Adds details of how to find the .zip file. ([46305](https://github.com/WordPress/gutenberg/pull/46305)) +- Doc: Fix description and documentation for link color support. ([46405](https://github.com/WordPress/gutenberg/pull/46405)) +- Docs: Add missing useState import in BorderBoxControl documentation. ([42067](https://github.com/WordPress/gutenberg/pull/42067)) +- Docs: Add missing useState import in color picker docs. ([42069](https://github.com/WordPress/gutenberg/pull/42069)) +- Docs: Add missing useState import in confirm dialog docs. ([42071](https://github.com/WordPress/gutenberg/pull/42071)) +- Docs: Adds reminder to use Node.js v14 in Quick Start. ([46216](https://github.com/WordPress/gutenberg/pull/46216)) +- Docs: Fix missing link to `primitives` package. ([46290](https://github.com/WordPress/gutenberg/pull/46290)) +- Docs: Update reference to IE 11. ([46296](https://github.com/WordPress/gutenberg/pull/46296)) + +### Code Quality +- Block Editor: Fix `no-node-access` violations in `BlockPreview`. ([46409](https://github.com/WordPress/gutenberg/pull/46409)) +- Block Editor: Fix `no-node-access` violations in `BlockSelectionClearer`. ([46408](https://github.com/WordPress/gutenberg/pull/46408)) +- Columns mobile edit: Remove unused updateBlockSettings action bind. ([46455](https://github.com/WordPress/gutenberg/pull/46455)) +- ESLint: Fix warning in `getBlockAttribute` documentation. ([46500](https://github.com/WordPress/gutenberg/pull/46500)) +- List View: Use default parameters instead of defaultProps. ([46266](https://github.com/WordPress/gutenberg/pull/46266)) +- Removed: Remove small APIs marked to be removed in WP 6.2. ([46106](https://github.com/WordPress/gutenberg/pull/46106)) +- Site Editor: Remove invalid CSS. ([46288](https://github.com/WordPress/gutenberg/pull/46288)) + +#### Block Library +- Group Block: Remove placeholder leftovers. ([46423](https://github.com/WordPress/gutenberg/pull/46423)) +- Group: Remove unnecessary 'useCallback'. ([46418](https://github.com/WordPress/gutenberg/pull/46418)) +- Navigation Block: Add tests for Nav block uncontrolled blocks dirty state checking. ([46329](https://github.com/WordPress/gutenberg/pull/46329)) +- Navigation Block: Update attribute test for `are-blocks-dirty.js`. ([46355](https://github.com/WordPress/gutenberg/pull/46355)) +- Page List Block: Move shared "convert" description to constant. ([46368](https://github.com/WordPress/gutenberg/pull/46368)) +- Page List Block: Simplify Page List convert to links function API. ([46365](https://github.com/WordPress/gutenberg/pull/46365)) +- Query: Cleanup variation picker component. ([46424](https://github.com/WordPress/gutenberg/pull/46424)) +- RNMobile: Add an inline comment to clarify usage of 'hard' limit vs. unbounded query. ([46245](https://github.com/WordPress/gutenberg/pull/46245)) +- Shared standard Link UI component between Nav Link and Submenu blocks. ([46370](https://github.com/WordPress/gutenberg/pull/46370)) +- Template Parts: Remove unnecessary 'useCallback'. ([46420](https://github.com/WordPress/gutenberg/pull/46420)) + +#### Components +- AlignmentMatrixControl: Refactor to TypeScript. ([46162](https://github.com/WordPress/gutenberg/pull/46162)) +- Also ignore `no-node-access` for some components. ([46501](https://github.com/WordPress/gutenberg/pull/46501)) +- Fix `no-node-access` violations in `FocalPointPicker` tests. ([46312](https://github.com/WordPress/gutenberg/pull/46312)) +- Fix `no-node-access` violations in `Popover`. ([46311](https://github.com/WordPress/gutenberg/pull/46311)) +- Fix `no-node-access` violations in `Theme`. ([46310](https://github.com/WordPress/gutenberg/pull/46310)) +- Fix `no-node-access` violations in `ToolsPanel` tests. ([46313](https://github.com/WordPress/gutenberg/pull/46313)) +- withFilters: Use 'act' from React Testing Library. ([46237](https://github.com/WordPress/gutenberg/pull/46237)) + +#### Data Layer +- Data: Add ability to subscribe to one store, remove __unstableSubscribeStore. ([45513](https://github.com/WordPress/gutenberg/pull/45513)) +- ESLint: Fix warnings in the data package. ([46499](https://github.com/WordPress/gutenberg/pull/46499)) + +#### Global Styles +- Add "custom-css" as an acceptable value in the documentation for gutenberg_get_global_stylesheet. ([46493](https://github.com/WordPress/gutenberg/pull/46493)) +- PaletteEdit: Add changelog. ([46095](https://github.com/WordPress/gutenberg/pull/46095)) + +#### Block Editor +- Inserter: Update mobile tab navigation styles. ([46186](https://github.com/WordPress/gutenberg/pull/46186)) + +#### Layout +- Clarify inline comment about switching to `safecss_filter_attr`. ([46061](https://github.com/WordPress/gutenberg/pull/46061)) + +### Tools + +#### Build Tooling +- Adds Github Action to validate Gradle Wrapper. ([46247](https://github.com/WordPress/gutenberg/pull/46247)) +- Prevent api-fetch and core-data from being imported in the block editor package. ([46302](https://github.com/WordPress/gutenberg/pull/46302)) +- Serialize the map objects properly in the Redux dev tools. ([46282](https://github.com/WordPress/gutenberg/pull/46282)) + +#### Testing +- E2E: Fix flaky Block Switcher tests. ([46406](https://github.com/WordPress/gutenberg/pull/46406)) +- end-to-end tests: Add width and color test to button block. ([46452](https://github.com/WordPress/gutenberg/pull/46452)) + + +## First time contributors + +The following PRs were merged by first time contributors: + +- @corentin-gautier: Avoid paint on popover when hovering content. ([46201](https://github.com/WordPress/gutenberg/pull/46201)) +- @ingeniumed: Expose before filter hook in useSettings for injecting block settings in the editor. ([45089](https://github.com/WordPress/gutenberg/pull/45089)) +- @janusqa: Reusable block: Pluralize the message "Convert to regular blocks" depending on the number of blocks contained. ([45819](https://github.com/WordPress/gutenberg/pull/45819)) + + +## Contributors + +The following contributors merged PRs in this release: + +@aaronrobertshaw @ajlende @andrewserong @aristath @chad1008 @chintu51 @corentin-gautier @derekblank @draganescu @ellatrix @geriux @getdave @glendaviesnz @hideokamoto @ingeniumed @jameskoster @janusqa @jasmussen @jffng @jorgefilipecosta @jsnajdr @madhusudhand @MaggieCabrera @Mamaduka @matiasbenedetto @mburridge @mikachan @mirka @noisysocks @ntsekouras @oandregal @oguzkocer @ramonjd @scruffian @SiobhyB @spacedmonkey @t-hamano @talldan @tellthemachines @tyxla @WunderBart @youknowriad + + += 14.7.3 = + +## Changelog + +### Bug fixes + +- Fix typing performance issue for container blocks. ([46527](https://github.com/WordPress/gutenberg/pull/46527)) + +## Contributors + +The following contributors merged PRs in this release: + +@youknowriad + + += 14.7.2 = + + + +## Changelog + +### Bug Fixes + +- Fix fatal error when using the plugin with PHP 8 and WordPress 6.0 by checking if block_type asset properties are set. ([46488](https://github.com/WordPress/gutenberg/pull/46488)) + + +## First time contributors + +The following PRs were merged by first time contributors: + + + +## Contributors + +The following contributors merged PRs in this release: + +@noahtallen + + = 14.7.1 = diff --git a/docs/contributors/code/release.md b/docs/contributors/code/release.md index f463cb319f665..492813376b2c9 100644 --- a/docs/contributors/code/release.md +++ b/docs/contributors/code/release.md @@ -34,6 +34,10 @@ To release a release candidate (RC) version of the plugin, enter `rc`. To releas This will trigger a GitHub Actions (GHA) workflow that bumps the plugin version, builds the Gutenberg plugin .zip file, creates a release draft, and attaches the plugin .zip file to it. This part of the process typically takes a little under six minutes. You'll see that workflow appear at the top of the list, right under the blue banner. Once it's finished, it'll change its status icon from a yellow dot to a green checkmark. You can follow along in a more detailed view by clicking on the workflow. +#### Publishing the @wordpress packages to NPM + +As part of the release candidate (RC) process, all of the `@wordpress` packages are published to NPM. You may see messaging after the ["Build Gutenberg Plugin Zip" action](https://github.com/WordPress/gutenberg/actions/workflows/build-plugin-zip.yml) action has created the draft release that the ["Publish npm packages"](https://github.com/WordPress/gutenberg/actions/workflows/publish-npm-packages.yml) action requires someone with appropriate permissions to trigger the action. This is not the case as this process is automated and it will automatically run after the release notes are published. + #### View the release draft As soon as the workflow has finished, you'll find the release draft under [https://github.com/WordPress/gutenberg/releases](https://github.com/WordPress/gutenberg/releases). The draft is pre-populated with changelog entries based on previous release candidates for this version, and any changes that have since been cherry-picked to the release branch. Thus, when releasing the first stable version of a series, make sure to delete any RC version headers (that are only there for your information) and to move the more recent changes to the correct section (see below). diff --git a/docs/reference-guides/core-blocks.md b/docs/reference-guides/core-blocks.md index 5320ec1731941..ce574465a5a15 100644 --- a/docs/reference-guides/core-blocks.md +++ b/docs/reference-guides/core-blocks.md @@ -428,7 +428,7 @@ Display a list of all pages. ([Source](https://github.com/WordPress/gutenberg/tr - **Name:** core/page-list - **Category:** widgets -- **Supports:** ~~html~~, ~~reusable~~ +- **Supports:** typography (fontSize, lineHeight), ~~html~~, ~~reusable~~ - **Attributes:** parentPageID ## Page List Item @@ -572,7 +572,7 @@ Contains the block elements used to render a post, like the title, date, feature - **Name:** core/post-template - **Category:** theme -- **Supports:** align, typography (fontSize, lineHeight), ~~html~~, ~~reusable~~ +- **Supports:** align, color (background, gradients, link, text), typography (fontSize, lineHeight), ~~html~~, ~~reusable~~ - **Attributes:** ## Post Terms @@ -617,7 +617,7 @@ An advanced block that allows displaying post types based on different query par - **Name:** core/query - **Category:** theme -- **Supports:** align (full, wide), color (background, gradients, link, text), ~~html~~ +- **Supports:** align (full, wide), ~~html~~ - **Attributes:** displayLayout, namespace, query, queryId, tagName ## No results diff --git a/docs/reference-guides/data/data-core-block-editor.md b/docs/reference-guides/data/data-core-block-editor.md index 5123dc6adc131..0580aa0141b2b 100644 --- a/docs/reference-guides/data/data-core-block-editor.md +++ b/docs/reference-guides/data/data-core-block-editor.md @@ -556,6 +556,18 @@ _Properties_ - _isDisabled_ `boolean`: Whether or not the user should be prevented from inserting this item. - _frecency_ `number`: Heuristic that combines frequency and recency. +### getLastInsertedBlockClientId + +Gets the client id of the last inserted block. + +_Parameters_ + +- _state_ `Object`: Global application state. + +_Returns_ + +- `string|undefined`: Client Id of the last inserted block. + ### getLastMultiSelectedBlockClientId Returns the client ID of the last block in the multi-selection set, or null diff --git a/docs/reference-guides/theme-json-reference/theme-json-living.md b/docs/reference-guides/theme-json-reference/theme-json-living.md index a694cf63e909f..9b074edace6ae 100644 --- a/docs/reference-guides/theme-json-reference/theme-json-living.md +++ b/docs/reference-guides/theme-json-reference/theme-json-living.md @@ -119,7 +119,7 @@ Settings related to typography. | customFontSize | boolean | true | | | fontStyle | boolean | true | | | fontWeight | boolean | true | | -| fluid | boolean | | | +| fluid | undefined | false | | | letterSpacing | boolean | true | | | lineHeight | boolean | false | | | textDecoration | boolean | true | | diff --git a/gutenberg.php b/gutenberg.php index c5f94c50522db..4d4489ef4c061 100644 --- a/gutenberg.php +++ b/gutenberg.php @@ -5,7 +5,7 @@ * Description: Printing since 1440. This is the development plugin for the new block editor in core. * Requires at least: 6.0 * Requires PHP: 5.6 - * Version: 14.7.1 + * Version: 14.8.0-rc.1 * Author: Gutenberg Team * Text Domain: gutenberg * diff --git a/lib/block-supports/typography.php b/lib/block-supports/typography.php index 01d223b84281e..809fba1a6e7ac 100644 --- a/lib/block-supports/typography.php +++ b/lib/block-supports/typography.php @@ -451,18 +451,25 @@ function gutenberg_get_typography_font_size_value( $preset, $should_use_fluid_ty // Checks if fluid font sizes are activated. $typography_settings = gutenberg_get_global_settings( array( 'typography' ) ); - $should_use_fluid_typography = isset( $typography_settings['fluid'] ) && true === $typography_settings['fluid'] ? true : $should_use_fluid_typography; + $should_use_fluid_typography + = isset( $typography_settings['fluid'] ) && + ( true === $typography_settings['fluid'] || is_array( $typography_settings['fluid'] ) ) ? + true : + $should_use_fluid_typography; if ( ! $should_use_fluid_typography ) { return $preset['size']; } + $fluid_settings = isset( $typography_settings['fluid'] ) && is_array( $typography_settings['fluid'] ) ? $typography_settings['fluid'] : array(); + // Defaults. $default_maximum_viewport_width = '1600px'; $default_minimum_viewport_width = '768px'; $default_minimum_font_size_factor = 0.75; $default_scale_factor = 1; - $default_minimum_font_size_limit = '14px'; + $has_min_font_size = isset( $fluid_settings['minFontSize'] ) && ! empty( gutenberg_get_typography_value_and_unit( $fluid_settings['minFontSize'] ) ); + $default_minimum_font_size_limit = $has_min_font_size ? $fluid_settings['minFontSize'] : '14px'; // Font sizes. $fluid_font_size_settings = isset( $preset['fluid'] ) ? $preset['fluid'] : null; diff --git a/lib/compat/wordpress-6.1/class-gutenberg-rest-block-patterns-controller.php b/lib/compat/wordpress-6.1/class-gutenberg-rest-block-patterns-controller-6-1.php similarity index 60% rename from lib/compat/wordpress-6.1/class-gutenberg-rest-block-patterns-controller.php rename to lib/compat/wordpress-6.1/class-gutenberg-rest-block-patterns-controller-6-1.php index c053a678083da..b40f3aa2497f1 100644 --- a/lib/compat/wordpress-6.1/class-gutenberg-rest-block-patterns-controller.php +++ b/lib/compat/wordpress-6.1/class-gutenberg-rest-block-patterns-controller-6-1.php @@ -1,6 +1,6 @@ namespace = 'wp/v2'; - $this->rest_base = 'block-patterns/patterns'; - } - - /** - * Registers the routes for the objects of the controller. - * - * @since 6.0.0 - */ - public function register_routes() { - register_rest_route( - $this->namespace, - '/' . $this->rest_base, - array( - array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( $this, 'get_items' ), - 'permission_callback' => array( $this, 'get_items_permissions_check' ), - ), - 'schema' => array( $this, 'get_public_item_schema' ), - ), - true - ); - } - - /** - * Checks whether a given request has permission to read block patterns. - * - * @since 6.0.0 - * - * @param WP_REST_Request $request Full details about the request. - * @return true|WP_Error True if the request has read access, WP_Error object otherwise. - */ - public function get_items_permissions_check( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable - if ( current_user_can( 'edit_posts' ) ) { - return true; - } - - foreach ( get_post_types( array( 'show_in_rest' => true ), 'objects' ) as $post_type ) { - if ( current_user_can( $post_type->cap->edit_posts ) ) { - return true; - } - } - - return new WP_Error( - 'rest_cannot_view', - __( 'Sorry, you are not allowed to view the registered block patterns.', 'gutenberg' ), - array( 'status' => rest_authorization_required_code() ) - ); - } - - /** - * Retrieves all block patterns. - * - * @since 6.0.0 - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. - */ - public function get_items( $request ) { - if ( ! $this->remote_patterns_loaded ) { - // Load block patterns from w.org. - _load_remote_block_patterns(); // Patterns with the `core` keyword. - _load_remote_featured_patterns(); // Patterns in the `featured` category. - _register_remote_theme_patterns(); // Patterns requested by current theme. - - $this->remote_patterns_loaded = true; - } - - $response = array(); - $patterns = WP_Block_Patterns_Registry::get_instance()->get_all_registered(); - foreach ( $patterns as $pattern ) { - $prepared_pattern = $this->prepare_item_for_response( $pattern, $request ); - $response[] = $this->prepare_response_for_collection( $prepared_pattern ); - } - return rest_ensure_response( $response ); - } - +class Gutenberg_REST_Block_Patterns_Controller_6_1 extends WP_REST_Block_Patterns_Controller { /** * Prepare a raw block pattern before it gets output in a REST API response. * * @since 6.0.0 + * @since 6.1.0 Added `postTypes` property. * * @param array $item Raw pattern as registered, before any changes. * @param WP_REST_Request $request Request object. @@ -147,6 +55,7 @@ public function prepare_item_for_response( $item, $request ) { * Retrieves the block pattern schema, conforming to JSON Schema. * * @since 6.0.0 + * @since 6.1.0 Added `post_types` property. * * @return array Item schema data. */ diff --git a/lib/compat/wordpress-6.1/rest-api.php b/lib/compat/wordpress-6.1/rest-api.php index f3391389d9e38..5dd06d3aad855 100644 --- a/lib/compat/wordpress-6.1/rest-api.php +++ b/lib/compat/wordpress-6.1/rest-api.php @@ -35,15 +35,6 @@ function gutenberg_update_post_types_rest_response( $response, $post_type ) { } add_filter( 'rest_prepare_post_type', 'gutenberg_update_post_types_rest_response', 10, 2 ); -/** - * Registers the block patterns REST API routes. - */ -function gutenberg_register_gutenberg_rest_block_patterns() { - $block_patterns = new Gutenberg_REST_Block_Patterns_Controller(); - $block_patterns->register_routes(); -} -add_action( 'rest_api_init', 'gutenberg_register_gutenberg_rest_block_patterns', 100 ); - /** * Exposes the site logo URL through the WordPress REST API. * diff --git a/lib/compat/wordpress-6.2/block-editor-settings.php b/lib/compat/wordpress-6.2/block-editor-settings.php new file mode 100644 index 0000000000000..7323d34eb667c --- /dev/null +++ b/lib/compat/wordpress-6.2/block-editor-settings.php @@ -0,0 +1,28 @@ + gutenberg_get_global_stylesheet( array( 'custom-css' ) ), + '__unstableType' => 'user', + 'isGlobalStyles' => true, + ); + } + + return $settings; +} + +add_filter( 'block_editor_settings_all', 'gutenberg_get_block_editor_settings_6_2', PHP_INT_MAX ); diff --git a/lib/compat/wordpress-6.2/block-patterns.php b/lib/compat/wordpress-6.2/block-patterns.php index 8f50c9ff33c5d..1e529faf96321 100644 --- a/lib/compat/wordpress-6.2/block-patterns.php +++ b/lib/compat/wordpress-6.2/block-patterns.php @@ -8,7 +8,7 @@ /** * Registers the block pattern categories. */ -function gutenberg_register_core_block_patterns_and_categories() { +function gutenberg_register_core_block_patterns_categories() { register_block_pattern_category( 'banner', array( @@ -30,49 +30,115 @@ function gutenberg_register_core_block_patterns_and_categories() { ) ); register_block_pattern_category( - 'footer', + 'text', array( - 'label' => _x( 'Footers', 'Block pattern category', 'gutenberg' ), - 'description' => __( 'A variety of footer designs displaying information and site navigation.', 'gutenberg' ), + 'label' => _x( 'Text', 'Block pattern category', 'gutenberg' ), + 'description' => __( 'Patterns containing mostly text.', 'gutenberg' ), ) ); register_block_pattern_category( - 'gallery', + 'query', array( - 'label' => _x( 'Gallery', 'Block pattern category', 'gutenberg' ), - 'description' => __( 'Patterns containing mostly images or other media.', 'gutenberg' ), + 'label' => _x( 'Posts', 'Block pattern category', 'gutenberg' ), + 'description' => __( 'Display your latest posts in lists, grids or other layouts.', 'gutenberg' ), ) ); register_block_pattern_category( - 'header', + 'featured', array( - 'label' => _x( 'Headers', 'Block pattern category', 'gutenberg' ), - 'description' => __( 'A variety of header designs displaying your site title and navigation.', 'gutenberg' ), + 'label' => _x( 'Featured', 'Block pattern category', 'gutenberg' ), + 'description' => __( 'A set of high quality curated patterns.', 'gutenberg' ), ) ); + + // Register new core block pattern categories. register_block_pattern_category( - 'text', + 'call-to-action', array( - 'label' => _x( 'Text', 'Block pattern category', 'gutenberg' ), - 'description' => __( 'Patterns containing mostly text.', 'gutenberg' ), + 'label' => _x( 'Call to Action', 'Block pattern category', 'gutenberg' ), + 'description' => __( 'Sections whose purpose is to trigger a specific action.', 'gutenberg' ), ) ); register_block_pattern_category( - 'query', + 'team', + array( + 'label' => _x( 'Team', 'Block pattern category', 'gutenberg' ), + 'description' => __( 'A variety of designs to display your team members.', 'gutenberg' ), + ) + ); + register_block_pattern_category( + 'testimonials', + array( + 'label' => _x( 'Testimonials', 'Block pattern category', 'gutenberg' ), + 'description' => __( 'Share reviews and feedback about your brand/business.', 'gutenberg' ), + ) + ); + register_block_pattern_category( + 'services', + array( + 'label' => _x( 'Services', 'Block pattern category', 'gutenberg' ), + 'description' => __( 'Briefly describe what your business does and how you can help.', 'gutenberg' ), + ) + ); + register_block_pattern_category( + 'contact', + array( + 'label' => _x( 'Contact', 'Block pattern category', 'gutenberg' ), + 'description' => __( 'Display your contact information.', 'gutenberg' ), + ) + ); + register_block_pattern_category( + 'about', + array( + 'label' => _x( 'About', 'Block pattern category', 'gutenberg' ), + 'description' => __( 'Introduce yourself.', 'gutenberg' ), + ) + ); + register_block_pattern_category( + 'portfolio', + array( + 'label' => _x( 'Portfolio', 'Block pattern category', 'gutenberg' ), + 'description' => __( 'Showcase your latest work.', 'gutenberg' ), + ) + ); + register_block_pattern_category( + 'gallery', + array( + 'label' => _x( 'Gallery', 'Block pattern category', 'gutenberg' ), + 'description' => __( 'Different layouts for displaying images.', 'gutenberg' ), + ) + ); + register_block_pattern_category( + 'media', + array( + 'label' => _x( 'Media', 'Block pattern category', 'gutenberg' ), + 'description' => __( 'Different layouts containing video or audio.', 'gutenberg' ), + ) + ); + register_block_pattern_category( + 'posts', array( 'label' => _x( 'Posts', 'Block pattern category', 'gutenberg' ), 'description' => __( 'Display your latest posts in lists, grids or other layouts.', 'gutenberg' ), ) ); + // Site building pattern categories. register_block_pattern_category( - 'featured', + 'footer', array( - 'label' => _x( 'Featured', 'Block pattern category', 'gutenberg' ), - 'description' => __( 'A set of high quality curated patterns.', 'gutenberg' ), + 'label' => _x( 'Footers', 'Block pattern category', 'gutenberg' ), + 'description' => __( 'A variety of footer designs displaying information and site navigation.', 'gutenberg' ), + ) + ); + register_block_pattern_category( + 'header', + array( + 'label' => _x( 'Headers', 'Block pattern category', 'gutenberg' ), + 'description' => __( 'A variety of header designs displaying your site title and navigation.', 'gutenberg' ), ) ); } -add_action( 'init', 'gutenberg_register_core_block_patterns_and_categories' ); +add_action( 'init', 'gutenberg_register_core_block_patterns_categories' ); /** * Registers Gutenberg-bundled patterns, with a focus on headers and footers diff --git a/lib/compat/wordpress-6.2/class-gutenberg-rest-block-patterns-controller-6-2.php b/lib/compat/wordpress-6.2/class-gutenberg-rest-block-patterns-controller-6-2.php new file mode 100644 index 0000000000000..d34160edb2af0 --- /dev/null +++ b/lib/compat/wordpress-6.2/class-gutenberg-rest-block-patterns-controller-6-2.php @@ -0,0 +1,107 @@ + 'call-to-action', + 'columns' => 'text', + 'query' => 'posts', + ); + + /** + * Registers the routes for the objects of the controller. + * + * @since 6.0.0 + */ + public function register_routes() { + register_rest_route( + $this->namespace, + '/' . $this->rest_base, + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ), + true + ); + } + /** + * Retrieves all block patterns. + * + * @since 6.0.0 + * @since 6.2.0 Added migration for old core pattern categories to the new ones. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function get_items( $request ) { + if ( ! $this->remote_patterns_loaded ) { + // Load block patterns from w.org. + _load_remote_block_patterns(); // Patterns with the `core` keyword. + _load_remote_featured_patterns(); // Patterns in the `featured` category. + _register_remote_theme_patterns(); // Patterns requested by current theme. + + $this->remote_patterns_loaded = true; + } + + $response = array(); + $patterns = WP_Block_Patterns_Registry::get_instance()->get_all_registered(); + foreach ( $patterns as $pattern ) { + $migrated_pattern = $this->migrate_pattern_categories( $pattern ); + $prepared_pattern = $this->prepare_item_for_response( $migrated_pattern, $request ); + $response[] = $this->prepare_response_for_collection( $prepared_pattern ); + } + return rest_ensure_response( $response ); + } + + /** + * Migrates old core pattern categories to new ones. + * + * Core pattern categories are being revamped and we need to handle the migration + * to the new ones and ensure backwards compatibility. + * + * @since 6.2.0 + * + * @param array $pattern Raw pattern as registered, before applying any changes. + * @return array Migrated pattern. + */ + protected function migrate_pattern_categories( $pattern ) { + if ( isset( $pattern['categories'] ) && is_array( $pattern['categories'] ) ) { + foreach ( $pattern['categories'] as $i => $category ) { + if ( array_key_exists( $category, static::$categories_migration ) ) { + $pattern['categories'][ $i ] = static::$categories_migration[ $category ]; + } + } + } + return $pattern; + } +} diff --git a/lib/compat/wordpress-6.2/class-gutenberg-rest-global-styles-controller-6-2.php b/lib/compat/wordpress-6.2/class-gutenberg-rest-global-styles-controller-6-2.php new file mode 100644 index 0000000000000..684786ef22d76 --- /dev/null +++ b/lib/compat/wordpress-6.2/class-gutenberg-rest-global-styles-controller-6-2.php @@ -0,0 +1,204 @@ +post_content, true ); + $is_global_styles_user_theme_json = isset( $raw_config['isGlobalStylesUserThemeJSON'] ) && true === $raw_config['isGlobalStylesUserThemeJSON']; + $config = array(); + if ( $is_global_styles_user_theme_json ) { + $config = ( new WP_Theme_JSON_Gutenberg( $raw_config, 'custom' ) )->get_raw_data(); + } + + // Base fields for every post. + $data = array(); + $fields = $this->get_fields_for_response( $request ); + + if ( rest_is_field_included( 'id', $fields ) ) { + $data['id'] = $post->ID; + } + + if ( rest_is_field_included( 'title', $fields ) ) { + $data['title'] = array(); + } + if ( rest_is_field_included( 'title.raw', $fields ) ) { + $data['title']['raw'] = $post->post_title; + } + if ( rest_is_field_included( 'title.rendered', $fields ) ) { + add_filter( 'protected_title_format', array( $this, 'protected_title_format' ) ); + + $data['title']['rendered'] = get_the_title( $post->ID ); + + remove_filter( 'protected_title_format', array( $this, 'protected_title_format' ) ); + } + + if ( rest_is_field_included( 'settings', $fields ) ) { + $data['settings'] = ! empty( $config['settings'] ) && $is_global_styles_user_theme_json ? $config['settings'] : new stdClass(); + } + + if ( rest_is_field_included( 'styles', $fields ) ) { + $data['styles'] = ! empty( $config['styles'] ) && $is_global_styles_user_theme_json ? $config['styles'] : new stdClass(); + } + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) { + $links = $this->prepare_links( $post->ID ); + $response->add_links( $links ); + if ( ! empty( $links['self']['href'] ) ) { + $actions = $this->get_available_actions(); + $self = $links['self']['href']; + foreach ( $actions as $rel ) { + $response->add_link( $rel, $self ); + } + } + } + + return $response; + } + + /** + * Updates a single global style config. + * + * @since 5.9.0 + * @since 6.2.0 Added validation of styles.css property. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function update_item( $request ) { + $post_before = $this->get_post( $request['id'] ); + if ( is_wp_error( $post_before ) ) { + return $post_before; + } + + $changes = $this->prepare_item_for_database( $request ); + if ( is_wp_error( $changes ) ) { + return $changes; + } + + $result = wp_update_post( wp_slash( (array) $changes ), true, false ); + if ( is_wp_error( $result ) ) { + return $result; + } + + $post = get_post( $request['id'] ); + $fields_update = $this->update_additional_fields_for_object( $post, $request ); + if ( is_wp_error( $fields_update ) ) { + return $fields_update; + } + + wp_after_insert_post( $post, true, $post_before ); + + $response = $this->prepare_item_for_response( $post, $request ); + + return rest_ensure_response( $response ); + } + /** + * Prepares a single global styles config for update. + * + * @since 5.9.0 + * @since 6.2.0 Added validation of styles.css property. + * + * @param WP_REST_Request $request Request object. + * @return stdClass Changes to pass to wp_update_post. + */ + protected function prepare_item_for_database( $request ) { + $changes = new stdClass(); + $changes->ID = $request['id']; + $post = get_post( $request['id'] ); + $existing_config = array(); + if ( $post ) { + $existing_config = json_decode( $post->post_content, true ); + $json_decoding_error = json_last_error(); + if ( JSON_ERROR_NONE !== $json_decoding_error || ! isset( $existing_config['isGlobalStylesUserThemeJSON'] ) || + ! $existing_config['isGlobalStylesUserThemeJSON'] ) { + $existing_config = array(); + } + } + if ( isset( $request['styles'] ) || isset( $request['settings'] ) ) { + $config = array(); + if ( isset( $request['styles'] ) ) { + $config['styles'] = $request['styles']; + if ( isset( $request['styles']['css'] ) ) { + $validate_custom_css = $this->validate_custom_css( $request['styles']['css'] ); + if ( is_wp_error( $validate_custom_css ) ) { + return $validate_custom_css; + } + } + } elseif ( isset( $existing_config['styles'] ) ) { + $config['styles'] = $existing_config['styles']; + } + if ( isset( $request['settings'] ) ) { + $config['settings'] = $request['settings']; + } elseif ( isset( $existing_config['settings'] ) ) { + $config['settings'] = $existing_config['settings']; + } + $config['isGlobalStylesUserThemeJSON'] = true; + $config['version'] = WP_Theme_JSON_Gutenberg::LATEST_SCHEMA; + $changes->post_content = wp_json_encode( $config ); + } + // Post title. + if ( isset( $request['title'] ) ) { + if ( is_string( $request['title'] ) ) { + $changes->post_title = $request['title']; + } elseif ( ! empty( $request['title']['raw'] ) ) { + $changes->post_title = $request['title']['raw']; + } + } + return $changes; + } + + /** + * Validate style.css as valid CSS. + * + * Currently just checks for invalid markup. + * + * @since 6.2.0 + * + * @param string $css CSS to validate. + * @return true|WP_Error True if the input was validated, otherwise WP_Error. + */ + private function validate_custom_css( $css ) { + if ( preg_match( '# 400 ) + ); + } + return true; + } +} diff --git a/lib/compat/wordpress-6.2/class-wp-theme-json-6-2.php b/lib/compat/wordpress-6.2/class-wp-theme-json-6-2.php index 4c6a1f50612a8..fe01eb273066e 100644 --- a/lib/compat/wordpress-6.2/class-wp-theme-json-6-2.php +++ b/lib/compat/wordpress-6.2/class-wp-theme-json-6-2.php @@ -97,6 +97,25 @@ class WP_Theme_JSON_6_2 extends WP_Theme_JSON_6_1 { 'box-shadow' => array( 'shadow' ), ); + /** + * Indirect metadata for style properties that are not directly output. + * + * Each element is a direct mapping from a CSS property name to the + * path to the value in theme.json & block attributes. + * + * Indirect properties are not output directly by `compute_style_properties`, + * but are used elsewhere in the processing of global styles. The indirect + * property is used to validate whether or not a style value is allowed. + * + * @since 6.2.0 + * @var array + */ + const INDIRECT_PROPERTIES_METADATA = array( + 'gap' => array( 'spacing', 'blockGap' ), + 'column-gap' => array( 'spacing', 'blockGap', 'left' ), + 'row-gap' => array( 'spacing', 'blockGap', 'top' ), + ); + /** * The valid properties under the settings key. * @@ -175,6 +194,7 @@ class WP_Theme_JSON_6_2 extends WP_Theme_JSON_6_1 { * @since 6.1.0 Added new side properties for `border`, * added new property `shadow`, * updated `blockGap` to be allowed at any level. + * @since 6.2.0 Added new property `css`. * @var array */ const VALID_STYLES = array( @@ -215,5 +235,157 @@ class WP_Theme_JSON_6_2 extends WP_Theme_JSON_6_1 { 'textDecoration' => null, 'textTransform' => null, ), + 'css' => null, ); + + /** + * Processes a style node and returns the same node + * without the insecure styles. + * + * @since 5.9.0 + * @since 6.2.0 Allow indirect properties used outside of `compute_style_properties`. + * + * @param array $input Node to process. + * @return array + */ + protected static function remove_insecure_styles( $input ) { + $output = array(); + $declarations = static::compute_style_properties( $input ); + + foreach ( $declarations as $declaration ) { + if ( static::is_safe_css_declaration( $declaration['name'], $declaration['value'] ) ) { + $path = static::PROPERTIES_METADATA[ $declaration['name'] ]; + + // Check the value isn't an array before adding so as to not + // double up shorthand and longhand styles. + $value = _wp_array_get( $input, $path, array() ); + if ( ! is_array( $value ) ) { + _wp_array_set( $output, $path, $value ); + } + } + } + + // Ensure indirect properties not handled by `compute_style_properties` are allowed. + foreach ( static::INDIRECT_PROPERTIES_METADATA as $property => $path ) { + $value = _wp_array_get( $input, $path, array() ); + if ( + isset( $value ) && + ! is_array( $value ) && + static::is_safe_css_declaration( $property, $value ) + ) { + _wp_array_set( $output, $path, $value ); + } + } + + return $output; + } + + /** + * Returns the stylesheet that results of processing + * the theme.json structure this object represents. + * + * @param array $types Types of styles to load. Will load all by default. It accepts: + * 'variables': only the CSS Custom Properties for presets & custom ones. + * 'styles': only the styles section in theme.json. + * 'presets': only the classes for the presets. + * 'custom-css': only the css from global styles.css. + * @param array $origins A list of origins to include. By default it includes VALID_ORIGINS. + * @param array $options An array of options for now used for internal purposes only (may change without notice). + * The options currently supported are 'scope' that makes sure all style are scoped to a given selector, + * and root_selector which overwrites and forces a given selector to be used on the root node. + * @return string Stylesheet. + */ + public function get_stylesheet( $types = array( 'variables', 'styles', 'presets' ), $origins = null, $options = array() ) { + if ( null === $origins ) { + $origins = static::VALID_ORIGINS; + } + + if ( is_string( $types ) ) { + // Dispatch error and map old arguments to new ones. + _deprecated_argument( __FUNCTION__, '5.9' ); + if ( 'block_styles' === $types ) { + $types = array( 'styles', 'presets' ); + } elseif ( 'css_variables' === $types ) { + $types = array( 'variables' ); + } else { + $types = array( 'variables', 'styles', 'presets' ); + } + } + + $blocks_metadata = static::get_blocks_metadata(); + $style_nodes = static::get_style_nodes( $this->theme_json, $blocks_metadata ); + $setting_nodes = static::get_setting_nodes( $this->theme_json, $blocks_metadata ); + + $root_style_key = array_search( static::ROOT_BLOCK_SELECTOR, array_column( $style_nodes, 'selector' ), true ); + $root_settings_key = array_search( static::ROOT_BLOCK_SELECTOR, array_column( $setting_nodes, 'selector' ), true ); + + if ( ! empty( $options['scope'] ) ) { + foreach ( $setting_nodes as &$node ) { + $node['selector'] = static::scope_selector( $options['scope'], $node['selector'] ); + } + foreach ( $style_nodes as &$node ) { + $node['selector'] = static::scope_selector( $options['scope'], $node['selector'] ); + } + } + + if ( ! empty( $options['root_selector'] ) ) { + if ( false !== $root_settings_key ) { + $setting_nodes[ $root_settings_key ]['selector'] = $options['root_selector']; + } + if ( false !== $root_style_key ) { + $setting_nodes[ $root_style_key ]['selector'] = $options['root_selector']; + } + } + + $stylesheet = ''; + + if ( in_array( 'variables', $types, true ) ) { + $stylesheet .= $this->get_css_variables( $setting_nodes, $origins ); + } + + if ( in_array( 'styles', $types, true ) ) { + if ( false !== $root_style_key ) { + $stylesheet .= $this->get_root_layout_rules( $style_nodes[ $root_style_key ]['selector'], $style_nodes[ $root_style_key ] ); + } + $stylesheet .= $this->get_block_classes( $style_nodes ); + } elseif ( in_array( 'base-layout-styles', $types, true ) ) { + $root_selector = static::ROOT_BLOCK_SELECTOR; + $columns_selector = '.wp-block-columns'; + if ( ! empty( $options['scope'] ) ) { + $root_selector = static::scope_selector( $options['scope'], $root_selector ); + $columns_selector = static::scope_selector( $options['scope'], $columns_selector ); + } + if ( ! empty( $options['root_selector'] ) ) { + $root_selector = $options['root_selector']; + } + // Base layout styles are provided as part of `styles`, so only output separately if explicitly requested. + // For backwards compatibility, the Columns block is explicitly included, to support a different default gap value. + $base_styles_nodes = array( + array( + 'path' => array( 'styles' ), + 'selector' => $root_selector, + ), + array( + 'path' => array( 'styles', 'blocks', 'core/columns' ), + 'selector' => $columns_selector, + 'name' => 'core/columns', + ), + ); + + foreach ( $base_styles_nodes as $base_style_node ) { + $stylesheet .= $this->get_layout_styles( $base_style_node ); + } + } + + if ( in_array( 'presets', $types, true ) ) { + $stylesheet .= $this->get_preset_classes( $setting_nodes, $origins ); + } + + // Load the custom CSS last so it has the highest specificity. + if ( in_array( 'custom-css', $types, true ) ) { + $stylesheet .= _wp_array_get( $this->theme_json, array( 'styles', 'css' ) ); + } + + return $stylesheet; + } } diff --git a/lib/compat/wordpress-6.2/class-wp-theme-json-resolver-6-2.php b/lib/compat/wordpress-6.2/class-wp-theme-json-resolver-6-2.php index 2cce8cec7e46e..110c8bac7b147 100644 --- a/lib/compat/wordpress-6.2/class-wp-theme-json-resolver-6-2.php +++ b/lib/compat/wordpress-6.2/class-wp-theme-json-resolver-6-2.php @@ -168,4 +168,63 @@ public static function get_merged_data( $origin = 'custom' ) { $result->set_spacing_sizes(); return $result; } + + /** + * Returns the user's origin config. + * + * @since 6.2 Added check for the WP_Theme_JSON_Gutenberg class to prevent $user + * values set in core fron overriding the new custom css values added to VALID_STYLES. + * This does not need to be backported to core as the new VALID_STYLES[css] value will + * be added to core with 6.2. + * + * @return WP_Theme_JSON_Gutenberg Entity that holds styles for user data. + */ + public static function get_user_data() { + if ( null !== static::$user && static::$user instanceof WP_Theme_JSON_Gutenberg ) { + return static::$user; + } + + $config = array(); + $user_cpt = static::get_user_data_from_wp_global_styles( wp_get_theme() ); + + if ( array_key_exists( 'post_content', $user_cpt ) ) { + $decoded_data = json_decode( $user_cpt['post_content'], true ); + + $json_decoding_error = json_last_error(); + if ( JSON_ERROR_NONE !== $json_decoding_error ) { + trigger_error( 'Error when decoding a theme.json schema for user data. ' . json_last_error_msg() ); + /** + * Filters the data provided by the user for global styles & settings. + * + * @param WP_Theme_JSON_Data_Gutenberg Class to access and update the underlying data. + */ + $theme_json = apply_filters( 'wp_theme_json_data_user', new WP_Theme_JSON_Data_Gutenberg( $config, 'custom' ) ); + $config = $theme_json->get_data(); + return new WP_Theme_JSON_Gutenberg( $config, 'custom' ); + } + + // Very important to verify if the flag isGlobalStylesUserThemeJSON is true. + // If is not true the content was not escaped and is not safe. + if ( + is_array( $decoded_data ) && + isset( $decoded_data['isGlobalStylesUserThemeJSON'] ) && + $decoded_data['isGlobalStylesUserThemeJSON'] + ) { + unset( $decoded_data['isGlobalStylesUserThemeJSON'] ); + $config = $decoded_data; + } + } + + /** + * Filters the data provided by the user for global styles & settings. + * + * @param WP_Theme_JSON_Data_Gutenberg Class to access and update the underlying data. + */ + $theme_json = apply_filters( 'wp_theme_json_data_user', new WP_Theme_JSON_Data_Gutenberg( $config, 'custom' ) ); + $config = $theme_json->get_data(); + + static::$user = new WP_Theme_JSON_Gutenberg( $config, 'custom' ); + + return static::$user; + } } diff --git a/lib/compat/wordpress-6.2/get-global-styles-and-settings.php b/lib/compat/wordpress-6.2/get-global-styles-and-settings.php index 1bf1ea23b6417..8556a0be1663f 100644 --- a/lib/compat/wordpress-6.2/get-global-styles-and-settings.php +++ b/lib/compat/wordpress-6.2/get-global-styles-and-settings.php @@ -63,7 +63,7 @@ function wp_theme_has_theme_json_clean_cache() { * Returns the stylesheet resulting of merging core, theme, and user data. * * @param array $types Types of styles to load. Optional. - * It accepts 'variables', 'styles', 'presets' as values. + * It accepts 'variables', 'styles', 'presets', 'custom-css' as values. * If empty, it'll load all for themes with theme.json support * and only [ 'variables', 'presets' ] for themes without theme.json support. * @@ -85,7 +85,7 @@ function gutenberg_get_global_stylesheet( $types = array() ) { if ( empty( $types ) && ! $supports_theme_json ) { $types = array( 'variables', 'presets', 'base-layout-styles' ); } elseif ( empty( $types ) ) { - $types = array( 'variables', 'styles', 'presets' ); + $types = array( 'variables', 'styles', 'presets', 'custom-css' ); } /* diff --git a/lib/compat/wordpress-6.2/rest-api.php b/lib/compat/wordpress-6.2/rest-api.php index 023a79e3db95f..12f7afda3b4d5 100644 --- a/lib/compat/wordpress-6.2/rest-api.php +++ b/lib/compat/wordpress-6.2/rest-api.php @@ -23,6 +23,15 @@ function gutenberg_register_rest_pattern_directory() { } add_action( 'rest_api_init', 'gutenberg_register_rest_pattern_directory' ); +/** + * Registers the block patterns REST API routes. + */ +function gutenberg_register_rest_block_patterns() { + $block_patterns = new Gutenberg_REST_Block_Patterns_Controller_6_2(); + $block_patterns->register_routes(); +} +add_action( 'rest_api_init', 'gutenberg_register_rest_block_patterns' ); + /** * Add extra collection params to pattern directory requests. * @@ -83,3 +92,27 @@ function gutenberg_pattern_directory_collection_params_6_2( $query_params ) { return $query_params; } add_filter( 'rest_pattern_directory_collection_params', 'gutenberg_pattern_directory_collection_params_6_2' ); + +/** + * Registers the Global Styles REST API routes. + */ +function gutenberg_register_global_styles_endpoints() { + $editor_settings = new Gutenberg_REST_Global_Styles_Controller_6_2(); + $editor_settings->register_routes(); +} +add_action( 'rest_api_init', 'gutenberg_register_global_styles_endpoints' ); + +/** + * Updates REST API response for the sidebars and marks them as 'inactive'. + * + * Note: This can be a part of the `prepare_item_for_response` in `class-wp-rest-sidebars-controller.php`. + * + * @param WP_REST_Response $response The sidebar response object. + * @return WP_REST_Response $response Updated response object. + */ +function gutenberg_modify_rest_sidebars_response( $response ) { + $response->data['status'] = wp_is_block_theme() ? 'inactive' : 'active'; + + return $response; +} +add_filter( 'rest_prepare_sidebar', 'gutenberg_modify_rest_sidebars_response' ); diff --git a/lib/compat/wordpress-6.2/script-loader.php b/lib/compat/wordpress-6.2/script-loader.php index 84681b3151238..e08bfa6e46ca8 100644 --- a/lib/compat/wordpress-6.2/script-loader.php +++ b/lib/compat/wordpress-6.2/script-loader.php @@ -90,16 +90,18 @@ function gutenberg_resolve_assets_override() { $block_registry = WP_Block_Type_Registry::get_instance(); foreach ( $block_registry->get_all_registered() as $block_type ) { - $style_handles = array_merge( - $style_handles, - $block_type->style_handles, - $block_type->editor_style_handles - ); - - $script_handles = array_merge( - $script_handles, - $block_type->script_handles - ); + // In older WordPress versions, like 6.0, these properties are not defined. + if ( isset( $block_type->style_handles ) && is_array( $block_type->style_handles ) ) { + $style_handles = array_merge( $style_handles, $block_type->style_handles ); + } + + if ( isset( $block_type->editor_style_handles ) && is_array( $block_type->editor_style_handles ) ) { + $style_handles = array_merge( $style_handles, $block_type->editor_style_handles ); + } + + if ( isset( $block_type->script_handles ) && is_array( $block_type->script_handles ) ) { + $script_handles = array_merge( $script_handles, $block_type->script_handles ); + } } $style_handles = array_unique( $style_handles ); diff --git a/lib/compat/wordpress-6.2/theme.php b/lib/compat/wordpress-6.2/theme.php new file mode 100644 index 0000000000000..79d5520644947 --- /dev/null +++ b/lib/compat/wordpress-6.2/theme.php @@ -0,0 +1,23 @@ +is_block_theme() ) { + set_theme_mod( 'wp_legacy_sidebars', $wp_registered_sidebars ); + } +} +add_action( 'switch_theme', 'gutenberg_set_legacy_sidebars', 10, 2 ); diff --git a/lib/compat/wordpress-6.2/widgets.php b/lib/compat/wordpress-6.2/widgets.php new file mode 100644 index 0000000000000..19591ae64607e --- /dev/null +++ b/lib/compat/wordpress-6.2/widgets.php @@ -0,0 +1,32 @@ + array( 'post-editor', 'site-editor', 'widgets-editor' ), ), + '__experimentalBlockInspectorAnimation' => array( + 'description' => __( 'Whether to enable animation when showing and hiding the block inspector.', 'gutenberg' ), + 'type' => 'object', + 'context' => array( 'site-editor' ), + ), + 'alignWide' => array( 'description' => __( 'Enable/Disable Wide/Full Alignments.', 'gutenberg' ), 'type' => 'boolean', diff --git a/lib/experimental/kses.php b/lib/experimental/kses.php new file mode 100644 index 0000000000000..fd7531617939f --- /dev/null +++ b/lib/experimental/kses.php @@ -0,0 +1,66 @@ +; + const renderedIcon = ( + + ); const style = showColors ? { backgroundColor: icon && icon.background, diff --git a/packages/block-editor/src/components/block-inspector/index.js b/packages/block-editor/src/components/block-inspector/index.js index d676bf5dac6a5..48d5d982e7508 100644 --- a/packages/block-editor/src/components/block-inspector/index.js +++ b/packages/block-editor/src/components/block-inspector/index.js @@ -14,9 +14,10 @@ import { __experimentalHStack as HStack, __experimentalVStack as VStack, Button, + __unstableMotion as motion, } from '@wordpress/components'; import { useSelect, useDispatch } from '@wordpress/data'; -import { useMemo, useCallback } from '@wordpress/element'; +import { useMemo, useCallback, Fragment } from '@wordpress/element'; /** * Internal dependencies @@ -171,6 +172,24 @@ const BlockInspector = ( { showNoBlockSelectedMessage = true } ) => { const availableTabs = useInspectorControlsTabs( blockType?.name ); const showTabs = availableTabs?.length > 1; + const isOffCanvasNavigationEditorEnabled = + window?.__experimentalEnableOffCanvasNavigationEditor === true; + + const blockInspectorAnimationSettings = useSelect( + ( select ) => { + if ( isOffCanvasNavigationEditorEnabled ) { + const globalBlockInspectorAnimationSettings = + select( blockEditorStore ).getSettings() + .__experimentalBlockInspectorAnimation; + return globalBlockInspectorAnimationSettings?.[ + blockType.name + ]; + } + return null; + }, + [ selectedBlockClientId, isOffCanvasNavigationEditorEnabled, blockType ] + ); + if ( count > 1 ) { return (
@@ -231,11 +250,67 @@ const BlockInspector = ( { showNoBlockSelectedMessage = true } ) => { /> ); } + return ( - + ( + + { children } + + ) } + > + + + + + ); +}; + +const BlockInspectorSingleBlockWrapper = ( { animate, wrapper, children } ) => { + return animate ? wrapper( children ) : children; +}; + +const AnimatedContainer = ( { + blockInspectorAnimationSettings, + selectedBlockClientId, + children, +} ) => { + const animationOrigin = + blockInspectorAnimationSettings && + blockInspectorAnimationSettings.enterDirection === 'leftToRight' + ? -50 + : 50; + + return ( + + { children } + ); }; diff --git a/packages/block-editor/src/components/copy-handler/index.js b/packages/block-editor/src/components/copy-handler/index.js index 3e5a50a8fe49e..3881ff06f2bf3 100644 --- a/packages/block-editor/src/components/copy-handler/index.js +++ b/packages/block-editor/src/components/copy-handler/index.js @@ -7,6 +7,8 @@ import { pasteHandler, store as blocksStore, createBlock, + findTransform, + getBlockTransforms, } from '@wordpress/blocks'; import { documentHasSelection, @@ -84,6 +86,7 @@ export function useClipboardHandler() { __unstableIsSelectionCollapsed, __unstableIsSelectionMergeable, __unstableGetSelectedBlocksWithPartialSelection, + canInsertBlockType, } = useSelect( blockEditorStore ); const { flashBlock, @@ -91,6 +94,7 @@ export function useClipboardHandler() { replaceBlocks, __unstableDeleteSelection, __unstableExpandSelection, + insertBlocks, } = useDispatch( blockEditorStore ); const notifyCopy = useNotifyCopy(); @@ -201,13 +205,55 @@ export function useClipboardHandler() { __experimentalCanUserUseUnfilteredHTML: canUserUseUnfilteredHTML, } = getSettings(); - const { plainText, html } = getPasteEventData( event ); - const blocks = pasteHandler( { - HTML: html, - plainText, - mode: 'BLOCKS', - canUserUseUnfilteredHTML, - } ); + const { plainText, html, files } = getPasteEventData( event ); + let blocks = []; + + if ( files.length ) { + const fromTransforms = getBlockTransforms( 'from' ); + blocks = files + .reduce( ( accumulator, file ) => { + const transformation = findTransform( + fromTransforms, + ( transform ) => + transform.type === 'files' && + transform.isMatch( [ file ] ) + ); + if ( transformation ) { + accumulator.push( + transformation.transform( [ file ] ) + ); + } + return accumulator; + }, [] ) + .flat(); + } else { + blocks = pasteHandler( { + HTML: html, + plainText, + mode: 'BLOCKS', + canUserUseUnfilteredHTML, + } ); + } + + if ( selectedBlockClientIds.length === 1 ) { + const [ selectedBlockClientId ] = selectedBlockClientIds; + + if ( + blocks.every( ( block ) => + canInsertBlockType( + block.name, + selectedBlockClientId + ) + ) + ) { + insertBlocks( + blocks, + undefined, + selectedBlockClientId + ); + return; + } + } replaceBlocks( selectedBlockClientIds, diff --git a/packages/block-editor/src/components/font-sizes/fluid-utils.js b/packages/block-editor/src/components/font-sizes/fluid-utils.js index de8a27e3014e8..f5816e9823d7a 100644 --- a/packages/block-editor/src/components/font-sizes/fluid-utils.js +++ b/packages/block-editor/src/components/font-sizes/fluid-utils.js @@ -40,6 +40,7 @@ const DEFAULT_MINIMUM_FONT_SIZE_LIMIT = '14px'; * @param {?string} args.minimumFontSize Minimum font size for any clamp() calculation. Optional. * @param {?number} args.scaleFactor A scale factor to determine how fast a font scales within boundaries. Optional. * @param {?number} args.minimumFontSizeFactor How much to scale defaultFontSize by to derive minimumFontSize. Optional. + * @param {?string} args.minimumFontSizeLimit The smallest a calculated font size may be. Optional. * * @return {string|null} A font-size value using clamp(). */ @@ -51,8 +52,13 @@ export function getComputedFluidTypographyValue( { maximumViewPortWidth = DEFAULT_MAXIMUM_VIEWPORT_WIDTH, scaleFactor = DEFAULT_SCALE_FACTOR, minimumFontSizeFactor = DEFAULT_MINIMUM_FONT_SIZE_FACTOR, - minimumFontSizeLimit = DEFAULT_MINIMUM_FONT_SIZE_LIMIT, + minimumFontSizeLimit, } ) { + // Validate incoming settings and set defaults. + minimumFontSizeLimit = !! getTypographyValueAndUnit( minimumFontSizeLimit ) + ? minimumFontSizeLimit + : DEFAULT_MINIMUM_FONT_SIZE_LIMIT; + /* * Calculates missing minimumFontSize and maximumFontSize from * defaultFontSize if provided. diff --git a/packages/block-editor/src/components/inner-blocks/index.js b/packages/block-editor/src/components/inner-blocks/index.js index 4542afc797462..9d0b3f34bc772 100644 --- a/packages/block-editor/src/components/inner-blocks/index.js +++ b/packages/block-editor/src/components/inner-blocks/index.js @@ -7,7 +7,7 @@ import classnames from 'classnames'; * WordPress dependencies */ import { useViewportMatch, useMergeRefs } from '@wordpress/compose'; -import { forwardRef } from '@wordpress/element'; +import { forwardRef, useMemo } from '@wordpress/element'; import { useSelect } from '@wordpress/data'; import { getBlockSupport, @@ -102,6 +102,16 @@ function UncontrolledInnerBlocks( props ) { const { allowSizingOnChildren = false } = getBlockSupport( name, '__experimentalLayout' ) || {}; + const layout = useMemo( + () => ( { + ...__experimentalLayout, + ...( allowSizingOnChildren && { + allowSizingOnChildren: true, + } ), + } ), + [ __experimentalLayout, allowSizingOnChildren ] + ); + // This component needs to always be synchronous as it's the one changing // the async mode depending on the block selection. return ( @@ -110,12 +120,7 @@ function UncontrolledInnerBlocks( props ) { rootClientId={ clientId } renderAppender={ renderAppender } __experimentalAppenderTagName={ __experimentalAppenderTagName } - __experimentalLayout={ { - ...__experimentalLayout, - ...( allowSizingOnChildren && { - allowSizingOnChildren: true, - } ), - } } + __experimentalLayout={ layout } wrapperRef={ wrapperRef } placeholder={ placeholder } /> diff --git a/packages/block-editor/src/components/inserter/block-patterns-tab.js b/packages/block-editor/src/components/inserter/block-patterns-tab.js index a1e9c83989465..93b4ca40978e1 100644 --- a/packages/block-editor/src/components/inserter/block-patterns-tab.js +++ b/packages/block-editor/src/components/inserter/block-patterns-tab.js @@ -53,10 +53,17 @@ function usePatternsCategories() { ) ) .sort( ( { name: currentName }, { name: nextName } ) => { - if ( ! [ currentName, nextName ].includes( 'featured' ) ) { + if ( + ! [ currentName, nextName ].some( ( categoryName ) => + [ 'featured', 'text' ].includes( categoryName ) + ) + ) { return 0; } - return currentName === 'featured' ? -1 : 1; + // Move `featured` category to the top and `text` to the bottom. + return currentName === 'featured' || nextName === 'text' + ? -1 + : 1; } ); if ( diff --git a/packages/block-editor/src/components/inserter/hooks/use-insertion-point.js b/packages/block-editor/src/components/inserter/hooks/use-insertion-point.js index d2bbaa909d0a2..52c7f9ba23f83 100644 --- a/packages/block-editor/src/components/inserter/hooks/use-insertion-point.js +++ b/packages/block-editor/src/components/inserter/hooks/use-insertion-point.js @@ -39,6 +39,7 @@ function useInsertionPoint( { isAppender, onSelect, shouldFocusBlock = true, + selectBlockOnInsert = true, } ) { const { getSelectedBlock } = useSelect( blockEditorStore ); const { destinationRootClientId, destinationIndex } = useSelect( @@ -108,7 +109,7 @@ function useInsertionPoint( { blocks, destinationIndex, destinationRootClientId, - true, + selectBlockOnInsert, shouldFocusBlock || shouldForceFocusBlock ? 0 : null, meta ); @@ -122,7 +123,7 @@ function useInsertionPoint( { speak( message ); if ( onSelect ) { - onSelect(); + onSelect( blocks ); } }, [ diff --git a/packages/block-editor/src/components/inserter/index.js b/packages/block-editor/src/components/inserter/index.js index 59c04e24d2bbb..258faff2b826a 100644 --- a/packages/block-editor/src/components/inserter/index.js +++ b/packages/block-editor/src/components/inserter/index.js @@ -143,18 +143,31 @@ class Inserter extends Component { // Feel free to make them stable after a few releases. __experimentalIsQuick: isQuick, prioritizePatterns, + onSelectOrClose, + selectBlockOnInsert, } = this.props; if ( isQuick ) { return ( { + onSelect={ ( blocks ) => { + const firstBlock = + Array.isArray( blocks ) && blocks?.length + ? blocks[ 0 ] + : blocks; + if ( + onSelectOrClose && + typeof onSelectOrClose === 'function' + ) { + onSelectOrClose( firstBlock ); + } onClose(); } } rootClientId={ rootClientId } clientId={ clientId } isAppender={ isAppender } prioritizePatterns={ prioritizePatterns } + selectBlockOnInsert={ selectBlockOnInsert } /> ); } @@ -380,7 +393,7 @@ export default compose( [ if ( onSelectOrClose ) { onSelectOrClose( { - insertedBlockId: blockToInsert?.clientId, + clientId: blockToInsert?.clientId, } ); } diff --git a/packages/block-editor/src/components/inserter/index.native.js b/packages/block-editor/src/components/inserter/index.native.js index 7dcc0fdbef0e1..a3e6981e6ecfc 100644 --- a/packages/block-editor/src/components/inserter/index.native.js +++ b/packages/block-editor/src/components/inserter/index.native.js @@ -216,7 +216,7 @@ export class Inserter extends Component { } onInserterToggledAnnouncement( isOpen ) { - AccessibilityInfo.isScreenReaderEnabled().done( ( isEnabled ) => { + AccessibilityInfo.isScreenReaderEnabled().then( ( isEnabled ) => { if ( isEnabled ) { const isIOS = Platform.OS === 'ios'; const announcement = isOpen diff --git a/packages/block-editor/src/components/inserter/quick-inserter.js b/packages/block-editor/src/components/inserter/quick-inserter.js index 39a5ac5c75b84..540b51a4757e0 100644 --- a/packages/block-editor/src/components/inserter/quick-inserter.js +++ b/packages/block-editor/src/components/inserter/quick-inserter.js @@ -31,6 +31,7 @@ export default function QuickInserter( { clientId, isAppender, prioritizePatterns, + selectBlockOnInsert, } ) { const [ filterValue, setFilterValue ] = useState( '' ); const [ destinationRootClientId, onInsertBlocks ] = useInsertionPoint( { @@ -38,6 +39,7 @@ export default function QuickInserter( { rootClientId, clientId, isAppender, + selectBlockOnInsert, } ); const [ blockTypes ] = useBlockTypesState( destinationRootClientId, @@ -121,6 +123,7 @@ export default function QuickInserter( { maxBlockTypes={ SHOWN_BLOCK_TYPES } isDraggable={ false } prioritizePatterns={ prioritizePatterns } + selectBlockOnInsert={ selectBlockOnInsert } />
diff --git a/packages/block-editor/src/components/inserter/search-results.js b/packages/block-editor/src/components/inserter/search-results.js index dfd7a3d73312d..f55e49bd1cc80 100644 --- a/packages/block-editor/src/components/inserter/search-results.js +++ b/packages/block-editor/src/components/inserter/search-results.js @@ -50,6 +50,7 @@ function InserterSearchResults( { isDraggable = true, shouldFocusBlock = true, prioritizePatterns, + selectBlockOnInsert, } ) { const debouncedSpeak = useDebounce( speak, 500 ); @@ -60,6 +61,7 @@ function InserterSearchResults( { isAppender, insertionIndex: __experimentalInsertionIndex, shouldFocusBlock, + selectBlockOnInsert, } ); const [ blockTypes, diff --git a/packages/block-editor/src/components/off-canvas-editor/appender.js b/packages/block-editor/src/components/off-canvas-editor/appender.js index 646700143cf51..0fb19df664f01 100644 --- a/packages/block-editor/src/components/off-canvas-editor/appender.js +++ b/packages/block-editor/src/components/off-canvas-editor/appender.js @@ -11,8 +11,13 @@ import Inserter from '../inserter'; import { LinkUI } from './link-ui'; import { updateAttributes } from './update-attributes'; +const BLOCKS_WITH_LINK_UI_SUPPORT = [ + 'core/navigation-link', + 'core/navigation-submenu', +]; + export const Appender = forwardRef( ( props, ref ) => { - const [ insertedBlock, setInsertedBlock ] = useState(); + const [ insertedBlockClientId, setInsertedBlockClientId ] = useState(); const { hideInserter, clientId } = useSelect( ( select ) => { const { @@ -31,40 +36,55 @@ export const Appender = forwardRef( ( props, ref ) => { }; }, [] ); - const { insertedBlockAttributes } = useSelect( + const { insertedBlockAttributes, insertedBlockName } = useSelect( ( select ) => { - const { getBlockAttributes } = select( blockEditorStore ); + const { getBlockName, getBlockAttributes } = + select( blockEditorStore ); return { - insertedBlockAttributes: getBlockAttributes( insertedBlock ), + insertedBlockAttributes: getBlockAttributes( + insertedBlockClientId + ), + insertedBlockName: getBlockName( insertedBlockClientId ), }; }, - [ insertedBlock ] + [ insertedBlockClientId ] ); const { updateBlockAttributes } = useDispatch( blockEditorStore ); const setAttributes = - ( insertedBlockClientId ) => ( _updatedAttributes ) => { - updateBlockAttributes( insertedBlockClientId, _updatedAttributes ); + ( _insertedBlockClientId ) => ( _updatedAttributes ) => { + updateBlockAttributes( _insertedBlockClientId, _updatedAttributes ); }; + const maybeSetInsertedBlockOnInsertion = ( _insertedBlock ) => { + if ( ! _insertedBlock?.clientId ) { + return; + } + + setInsertedBlockClientId( _insertedBlock?.clientId ); + }; + let maybeLinkUI; - if ( insertedBlock ) { + if ( + insertedBlockClientId && + BLOCKS_WITH_LINK_UI_SUPPORT?.includes( insertedBlockName ) + ) { maybeLinkUI = ( setInsertedBlock( null ) } + onClose={ () => setInsertedBlockClientId( null ) } hasCreateSuggestion={ false } onChange={ ( updatedValue ) => { updateAttributes( updatedValue, - setAttributes( insertedBlock ), + setAttributes( insertedBlockClientId ), insertedBlockAttributes ); - setInsertedBlock( null ); + setInsertedBlockClientId( null ); } } /> ); @@ -77,15 +97,15 @@ export const Appender = forwardRef( ( props, ref ) => { return (
{ maybeLinkUI } + { - setInsertedBlock( insertedBlockId ); - } } + onSelectOrClose={ maybeSetInsertedBlockOnInsertion } + __experimentalIsQuick { ...props } />
diff --git a/packages/block-editor/src/components/off-canvas-editor/block-select-button.js b/packages/block-editor/src/components/off-canvas-editor/block-select-button.js index 9477eb2cda40c..a3cb64e9298dd 100644 --- a/packages/block-editor/src/components/off-canvas-editor/block-select-button.js +++ b/packages/block-editor/src/components/off-canvas-editor/block-select-button.js @@ -79,7 +79,11 @@ function ListViewBlockSelectButton( aria-hidden={ true } > - + select( blockEditorStore ).getBlockName( clientId ), + const block = useSelect( + ( select ) => select( blockEditorStore ).getBlock( clientId ), [ clientId ] ); + // If ListView has experimental features related to the Persistent List View, + // only focus the selected list item on mount; otherwise the list would always + // try to steal the focus from the editor canvas. + useEffect( () => { + if ( ! isTreeGridMounted && isSelected ) { + cellRef.current.focus(); + } + }, [] ); + + const onMouseEnter = useCallback( () => { + setIsHovered( true ); + toggleBlockHighlight( clientId, true ); + }, [ clientId, setIsHovered, toggleBlockHighlight ] ); + const onMouseLeave = useCallback( () => { + setIsHovered( false ); + toggleBlockHighlight( clientId, false ); + }, [ clientId, setIsHovered, toggleBlockHighlight ] ); + + const selectEditorBlock = useCallback( + ( event ) => { + selectBlock( event, clientId ); + event.preventDefault(); + }, + [ clientId, selectBlock ] + ); + + const updateSelection = useCallback( + ( newClientId ) => { + selectBlock( undefined, newClientId ); + }, + [ selectBlock ] + ); + + const { isTreeGridMounted, expand, collapse } = useListViewContext(); + + const toggleExpanded = useCallback( + ( event ) => { + // Prevent shift+click from opening link in a new window when toggling. + event.preventDefault(); + event.stopPropagation(); + if ( isExpanded === true ) { + collapse( clientId ); + } else if ( isExpanded === false ) { + expand( clientId ); + } + }, + [ clientId, expand, collapse, isExpanded ] + ); + + const instanceId = useInstanceId( ListViewBlock ); + + if ( ! block ) { + return null; + } + // When a block hides its toolbar it also hides the block settings menu, // since that menu is part of the toolbar in the editor canvas. // List View respects this by also hiding the block settings menu. - const showBlockActions = hasBlockSupport( - blockName, - '__experimentalToolbar', - true - ); - const instanceId = useInstanceId( ListViewBlock ); + const showBlockActions = + !! block && + hasBlockSupport( block.name, '__experimentalToolbar', true ); + const descriptionId = `list-view-block-select-button__${ instanceId }`; const blockPositionDescription = getBlockPositionDescription( position, @@ -143,9 +195,7 @@ function ListViewBlock( { ) : __( 'Edit' ); - const { isTreeGridMounted, expand, collapse } = useListViewContext(); - - const isEditable = block.name !== 'core/page-list-item'; + const isEditable = !! block && block.name !== 'core/page-list-item'; const hasSiblings = siblingBlockCount > 0; const hasRenderedMovers = showBlockMovers && hasSiblings; const moverCellClassName = classnames( @@ -163,53 +213,6 @@ function ListViewBlock( { { 'is-visible': isHovered || isFirstSelectedBlock } ); - // If ListView has experimental features related to the Persistent List View, - // only focus the selected list item on mount; otherwise the list would always - // try to steal the focus from the editor canvas. - useEffect( () => { - if ( ! isTreeGridMounted && isSelected ) { - cellRef.current.focus(); - } - }, [] ); - - const onMouseEnter = useCallback( () => { - setIsHovered( true ); - toggleBlockHighlight( clientId, true ); - }, [ clientId, setIsHovered, toggleBlockHighlight ] ); - const onMouseLeave = useCallback( () => { - setIsHovered( false ); - toggleBlockHighlight( clientId, false ); - }, [ clientId, setIsHovered, toggleBlockHighlight ] ); - - const selectEditorBlock = useCallback( - ( event ) => { - selectBlock( event, clientId ); - event.preventDefault(); - }, - [ clientId, selectBlock ] - ); - - const updateSelection = useCallback( - ( newClientId ) => { - selectBlock( undefined, newClientId ); - }, - [ selectBlock ] - ); - - const toggleExpanded = useCallback( - ( event ) => { - // Prevent shift+click from opening link in a new window when toggling. - event.preventDefault(); - event.stopPropagation(); - if ( isExpanded === true ) { - collapse( clientId ); - } else if ( isExpanded === false ) { - expand( clientId ); - } - }, - [ clientId, expand, collapse, isExpanded ] - ); - let colSpan; if ( hasRenderedMovers ) { colSpan = 2; @@ -325,27 +328,28 @@ function ListViewBlock( { { showBlockActions && ( <> - - { ( props ) => - isEditable && ( + { isEditable && ( + + { ( props ) => ( - ) - } - + ) } + + ) } { ( { ref, tabIndex, onFocus } ) => ( - { __( 'Add a submenu item' ) } + { __( 'Add submenu item' ) } ) } diff --git a/packages/block-editor/src/components/off-canvas-editor/link-ui.js b/packages/block-editor/src/components/off-canvas-editor/link-ui.js index 000ffc54f0ec2..f122d9a1e698f 100644 --- a/packages/block-editor/src/components/off-canvas-editor/link-ui.js +++ b/packages/block-editor/src/components/off-canvas-editor/link-ui.js @@ -75,6 +75,7 @@ function LinkControlTransforms( { clientId } ) { const { replaceBlock } = useDispatch( blockEditorStore ); const featuredBlocks = [ + 'core/page-list', 'core/site-logo', 'core/social-links', 'core/search', diff --git a/packages/block-editor/src/components/rich-text/file-paste-handler.js b/packages/block-editor/src/components/rich-text/file-paste-handler.js deleted file mode 100644 index 2aae5984389e6..0000000000000 --- a/packages/block-editor/src/components/rich-text/file-paste-handler.js +++ /dev/null @@ -1,13 +0,0 @@ -/** - * WordPress dependencies - */ -import { createBlobURL } from '@wordpress/blob'; - -export function filePasteHandler( files ) { - return files - .filter( ( { type } ) => - /^image\/(?:jpe?g|png|gif|webp)$/.test( type ) - ) - .map( ( file ) => `` ) - .join( '' ); -} diff --git a/packages/block-editor/src/components/rich-text/use-paste-handler.js b/packages/block-editor/src/components/rich-text/use-paste-handler.js index 5e4f4260f5001..67c932aceddcc 100644 --- a/packages/block-editor/src/components/rich-text/use-paste-handler.js +++ b/packages/block-editor/src/components/rich-text/use-paste-handler.js @@ -4,7 +4,11 @@ import { useRef } from '@wordpress/element'; import { useRefEffect } from '@wordpress/compose'; import { getFilesFromDataTransfer } from '@wordpress/dom'; -import { pasteHandler } from '@wordpress/blocks'; +import { + pasteHandler, + findTransform, + getBlockTransforms, +} from '@wordpress/blocks'; import { isEmpty, insert, @@ -17,7 +21,6 @@ import { isURL } from '@wordpress/url'; /** * Internal dependencies */ -import { filePasteHandler } from './file-paste-handler'; import { addActiveFormats, isShortcode } from './utils'; import { splitValue } from './split-value'; import { shouldDismissPastedFiles } from '../../utils/pasting'; @@ -155,6 +158,12 @@ export function usePasteHandler( props ) { return; } + if ( files?.length ) { + // Allows us to ask for this information when we get a report. + // eslint-disable-next-line no-console + window.console.log( 'Received items:\n\n', files ); + } + // Process any attached files, unless we infer that the files in // question are redundant "screenshots" of the actual HTML payload, // as created by certain office-type programs. @@ -164,23 +173,33 @@ export function usePasteHandler( props ) { files?.length && ! shouldDismissPastedFiles( files, html, plainText ) ) { - const content = pasteHandler( { - HTML: filePasteHandler( files ), - mode: 'BLOCKS', - tagName, - preserveWhiteSpace, - } ); - - // Allows us to ask for this information when we get a report. - // eslint-disable-next-line no-console - window.console.log( 'Received items:\n\n', files ); + const fromTransforms = getBlockTransforms( 'from' ); + const blocks = files + .reduce( ( accumulator, file ) => { + const transformation = findTransform( + fromTransforms, + ( transform ) => + transform.type === 'files' && + transform.isMatch( [ file ] ) + ); + if ( transformation ) { + accumulator.push( + transformation.transform( [ file ] ) + ); + } + return accumulator; + }, [] ) + .flat(); + if ( ! blocks.length ) { + return; + } if ( onReplace && isEmpty( value ) ) { - onReplace( content ); + onReplace( blocks ); } else { splitValue( { value, - pastedBlocks: content, + pastedBlocks: blocks, onReplace, onSplit, onSplitMiddle, diff --git a/packages/block-editor/src/components/spacing-sizes-control/index.js b/packages/block-editor/src/components/spacing-sizes-control/index.js index fb4ce2176b759..4ec1285db52bb 100644 --- a/packages/block-editor/src/components/spacing-sizes-control/index.js +++ b/packages/block-editor/src/components/spacing-sizes-control/index.js @@ -78,7 +78,6 @@ export default function SpacingSizesControl( { return (
{ - node.tabIndex = -1; + node.tabIndex = 0; node.contentEditable = hasMultiSelection; if ( ! hasMultiSelection ) { diff --git a/packages/block-editor/src/hooks/color.js b/packages/block-editor/src/hooks/color.js index 593eea881e310..fee6f39daa8b2 100644 --- a/packages/block-editor/src/hooks/color.js +++ b/packages/block-editor/src/hooks/color.js @@ -522,8 +522,6 @@ export function ColorEdit( props ) { allSolids, style?.elements?.link?.color?.text ), - clearable: - !! style?.elements?.link?.color?.text, isShownByDefault: defaultColorControls?.link, resetAllFilter: resetAllLinkFilter, }, diff --git a/packages/block-editor/src/hooks/font-size.js b/packages/block-editor/src/hooks/font-size.js index 6cb950afc4564..0c7a71fd23d68 100644 --- a/packages/block-editor/src/hooks/font-size.js +++ b/packages/block-editor/src/hooks/font-size.js @@ -324,13 +324,22 @@ function addEditPropsForFluidCustomFontSizes( blockType ) { // BlockListContext.Provider. If we set fontSize using editor. // BlockListBlock instead of using getEditWrapperProps then the value is // clobbered when the core/style/addEditProps filter runs. - const isFluidTypographyEnabled = - !! select( blockEditorStore ).getSettings().__experimentalFeatures + const fluidTypographyConfig = + select( blockEditorStore ).getSettings().__experimentalFeatures ?.typography?.fluid; + const fluidTypographySettings = + typeof fluidTypographyConfig === 'object' + ? fluidTypographyConfig + : {}; + const newFontSize = - fontSize && isFluidTypographyEnabled - ? getComputedFluidTypographyValue( { fontSize } ) + fontSize && !! fluidTypographyConfig + ? getComputedFluidTypographyValue( { + fontSize, + minimumFontSizeLimit: + fluidTypographySettings?.minFontSize, + } ) : null; if ( newFontSize === null ) { diff --git a/packages/block-editor/src/hooks/test/use-typography-props.js b/packages/block-editor/src/hooks/test/use-typography-props.js index 00557881467ca..12336eb2c44af 100644 --- a/packages/block-editor/src/hooks/test/use-typography-props.js +++ b/packages/block-editor/src/hooks/test/use-typography-props.js @@ -47,4 +47,30 @@ describe( 'getTypographyClassesAndStyles', () => { }, } ); } ); + + it( 'should return configured fluid font size styles', () => { + const attributes = { + fontFamily: 'tofu', + style: { + typography: { + textDecoration: 'underline', + fontSize: '2rem', + textTransform: 'uppercase', + }, + }, + }; + expect( + getTypographyClassesAndStyles( attributes, { + minFontSize: '1rem', + } ) + ).toEqual( { + className: 'has-tofu-font-family', + style: { + textDecoration: 'underline', + fontSize: + 'clamp(1.5rem, 1.5rem + ((1vw - 0.48rem) * 0.962), 2rem)', + textTransform: 'uppercase', + }, + } ); + } ); } ); diff --git a/packages/block-editor/src/hooks/use-typography-props.js b/packages/block-editor/src/hooks/use-typography-props.js index d70ae08aafc59..da5869ad9aec0 100644 --- a/packages/block-editor/src/hooks/use-typography-props.js +++ b/packages/block-editor/src/hooks/use-typography-props.js @@ -19,23 +19,31 @@ import { getComputedFluidTypographyValue } from '../components/font-sizes/fluid- * Provides the CSS class names and inline styles for a block's typography support * attributes. * - * @param {Object} attributes Block attributes. - * @param {boolean} isFluidFontSizeActive Whether the function should try to convert font sizes to fluid values. + * @param {Object} attributes Block attributes. + * @param {Object|boolean} fluidTypographySettings If boolean, whether the function should try to convert font sizes to fluid values, + * otherwise an object containing theme fluid typography settings. * * @return {Object} Typography block support derived CSS classes & styles. */ export function getTypographyClassesAndStyles( attributes, - isFluidFontSizeActive + fluidTypographySettings ) { let typographyStyles = attributes?.style?.typography || {}; - if ( isFluidFontSizeActive ) { + if ( + !! fluidTypographySettings && + ( true === fluidTypographySettings || + Object.keys( fluidTypographySettings ).length !== 0 ) + ) { + const newFontSize = + getComputedFluidTypographyValue( { + fontSize: attributes?.style?.typography?.fontSize, + minimumFontSizeLimit: fluidTypographySettings?.minFontSize, + } ) || attributes?.style?.typography?.fontSize; typographyStyles = { ...typographyStyles, - fontSize: getComputedFluidTypographyValue( { - fontSize: attributes?.style?.typography?.fontSize, - } ), + fontSize: newFontSize, }; } diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index 36d3f42ae58bc..3c048a7d58a29 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -2653,6 +2653,16 @@ export function wasBlockJustInserted( state, clientId, source ) { ); } +/** + * Gets the client id of the last inserted block. + * + * @param {Object} state Global application state. + * @return {string|undefined} Client Id of the last inserted block. + */ +export function getLastInsertedBlockClientId( state ) { + return state?.lastBlockInserted?.clientId; +} + /** * Tells if the block is visible on the canvas or not. * @@ -2690,8 +2700,8 @@ export const __unstableGetContentLockingParent = createSelector( ( state, clientId ) => { let current = clientId; let result; - while ( !! state.blocks.parents[ current ] ) { - current = state.blocks.parents[ current ]; + while ( state.blocks.parents.has( current ) ) { + current = state.blocks.parents.get( current ); if ( getTemplateLock( state, current ) === 'contentOnly' ) { result = current; } diff --git a/packages/block-editor/src/store/test/selectors.js b/packages/block-editor/src/store/test/selectors.js index 0086714d12804..ee4e9ee4c167a 100644 --- a/packages/block-editor/src/store/test/selectors.js +++ b/packages/block-editor/src/store/test/selectors.js @@ -73,6 +73,7 @@ const { __experimentalGetPatternTransformItems, wasBlockJustInserted, __experimentalGetGlobalBlocksByName, + getLastInsertedBlockClientId, } = selectors; describe( 'selectors', () => { @@ -4665,3 +4666,23 @@ describe( '__unstableGetClientIdsTree', () => { ] ); } ); } ); + +describe( 'getLastInsertedBlockClientId', () => { + it( 'should return undefined if no blocks have been inserted', () => { + const state = { + lastBlockInserted: {}, + }; + + expect( getLastInsertedBlockClientId( state ) ).toEqual( undefined ); + } ); + + it( 'should return clientId if blocks have been inserted', () => { + const state = { + lastBlockInserted: { + clientId: '123456', + }, + }; + + expect( getLastInsertedBlockClientId( state ) ).toEqual( '123456' ); + } ); +} ); diff --git a/packages/block-editor/src/utils/pasting.js b/packages/block-editor/src/utils/pasting.js index 366b79a329422..e962e11050a1d 100644 --- a/packages/block-editor/src/utils/pasting.js +++ b/packages/block-editor/src/utils/pasting.js @@ -1,7 +1,6 @@ /** * WordPress dependencies */ -import { createBlobURL } from '@wordpress/blob'; import { getFilesFromDataTransfer } from '@wordpress/dom'; export function getPasteEventData( { clipboardData } ) { @@ -25,21 +24,16 @@ export function getPasteEventData( { clipboardData } ) { } } - const files = getFilesFromDataTransfer( clipboardData ).filter( - ( { type } ) => /^image\/(?:jpe?g|png|gif|webp)$/.test( type ) - ); + const files = getFilesFromDataTransfer( clipboardData ); if ( files.length && ! shouldDismissPastedFiles( files, html, plainText ) ) { - html = files - .map( ( file ) => `` ) - .join( '' ); - plainText = ''; + return { files }; } - return { html, plainText }; + return { html, plainText, files: [] }; } /** diff --git a/packages/block-library/src/columns/edit.native.js b/packages/block-library/src/columns/edit.native.js index 96855e3da17e4..6331097f96b99 100644 --- a/packages/block-library/src/columns/edit.native.js +++ b/packages/block-library/src/columns/edit.native.js @@ -2,7 +2,7 @@ * External dependencies */ import { View, Dimensions } from 'react-native'; -import { map } from 'lodash'; + /** * WordPress dependencies */ @@ -332,11 +332,6 @@ const ColumnsEditContainerWrapper = withDispatch( width: value, } ); }, - updateBlockSettings( settings ) { - const { clientId } = ownProps; - const { updateBlockListSettings } = dispatch( blockEditorStore ); - updateBlockListSettings( clientId, settings ); - }, /** * Updates the column columnCount, including necessary revisions to child Column * blocks to grant required or redistribute available space. @@ -450,7 +445,7 @@ const ColumnsEdit = ( props ) => { const { columnCount, isDefaultColumns, - innerWidths = [], + innerBlocks, hasParents, parentBlockAlignment, editorSidebarOpened, @@ -463,24 +458,20 @@ const ColumnsEdit = ( props ) => { getBlockAttributes, } = select( blockEditorStore ); const { isEditorSidebarOpened } = select( 'core/edit-post' ); - const innerBlocks = getBlocks( clientId ); - const isContentEmpty = map( - innerBlocks, - ( innerBlock ) => innerBlock.innerBlocks.length + const innerBlocksList = getBlocks( clientId ); + + const isContentEmpty = innerBlocksList.every( + ( innerBlock ) => innerBlock.innerBlocks.length === 0 ); - const innerColumnsWidths = innerBlocks.map( ( inn ) => ( { - clientId: inn.clientId, - attributes: { width: inn.attributes.width }, - } ) ); const parents = getBlockParents( clientId, true ); return { columnCount: getBlockCount( clientId ), - isDefaultColumns: ! isContentEmpty.filter( Boolean ).length, - innerWidths: innerColumnsWidths, - hasParents: !! parents.length, + isDefaultColumns: isContentEmpty, + innerBlocks: innerBlocksList, + hasParents: parents.length > 0, parentBlockAlignment: getBlockAttributes( parents[ 0 ] )?.align, editorSidebarOpened: isSelected && isEditorSidebarOpened(), }; @@ -488,13 +479,14 @@ const ColumnsEdit = ( props ) => { [ clientId, isSelected ] ); - const memoizedInnerWidths = useMemo( () => { - return innerWidths; - }, [ - // The JSON.stringify is used because innerWidth is always a new reference. - // The innerBlocks is a new reference after each attribute change of any nested block. - JSON.stringify( innerWidths ), - ] ); + const innerWidths = useMemo( + () => + innerBlocks.map( ( inn ) => ( { + clientId: inn.clientId, + attributes: { width: inn.attributes.width }, + } ) ), + [ innerBlocks ] + ); const [ isVisible, setIsVisible ] = useState( false ); @@ -514,7 +506,7 @@ const ColumnsEdit = ( props ) => { 'wp-block-post-comments-editor', 'render_callback' => 'render_block_core_comments', 'skip_inner_blocks' => true, ); diff --git a/packages/block-library/src/gallery/edit.js b/packages/block-library/src/gallery/edit.js index 036f0c2133a5d..893167e6690f3 100644 --- a/packages/block-library/src/gallery/edit.js +++ b/packages/block-library/src/gallery/edit.js @@ -2,7 +2,6 @@ * External dependencies */ import classnames from 'classnames'; -import { find } from 'lodash'; /** * WordPress dependencies @@ -21,6 +20,7 @@ import { MediaPlaceholder, InspectorControls, useBlockProps, + useInnerBlocksProps, BlockControls, MediaReplaceFlow, } from '@wordpress/block-editor'; @@ -64,6 +64,7 @@ const linkOptions = [ }, ]; const ALLOWED_MEDIA_TYPES = [ 'image' ]; +const allowedBlocks = [ 'core/image' ]; const PLACEHOLDER_TEXT = Platform.isNative ? __( 'ADD MEDIA' ) @@ -174,7 +175,7 @@ function GalleryEdit( props ) { */ function buildImageAttributes( imageAttributes ) { const image = imageAttributes.id - ? find( imageData, { id: imageAttributes.id } ) + ? imageData.find( ( { id } ) => id === imageAttributes.id ) : null; let newClassName; @@ -220,7 +221,7 @@ function GalleryEdit( props ) { // It's necessary to retrieve the media type from the raw image data for already-uploaded images on native. const nativeFileData = Platform.isNative && file.id - ? find( imageData, { id: file.id } ) + ? imageData.find( ( { id } ) => id === file.id ) : null; const mediaTypeSelector = nativeFileData @@ -333,7 +334,7 @@ function GalleryEdit( props ) { getBlock( clientId ).innerBlocks.forEach( ( block ) => { blocks.push( block.clientId ); const image = block.attributes.id - ? find( imageData, { id: block.attributes.id } ) + ? imageData.find( ( { id } ) => id === block.attributes.id ) : null; changedAttributes[ block.clientId ] = getHrefAndDestination( image, @@ -401,7 +402,7 @@ function GalleryEdit( props ) { getBlock( clientId ).innerBlocks.forEach( ( block ) => { blocks.push( block.clientId ); const image = block.attributes.id - ? find( imageData, { id: block.attributes.id } ) + ? imageData.find( ( { id } ) => id === block.attributes.id ) : null; changedAttributes[ block.clientId ] = getImageSizeAttributes( image, @@ -484,8 +485,20 @@ function GalleryEdit( props ) { className: classnames( className, 'has-nested-images' ), } ); + const innerBlocksProps = useInnerBlocksProps( blockProps, { + allowedBlocks, + orientation: 'horizontal', + renderAppender: false, + __experimentalLayout: { type: 'default', alignments: [] }, + } ); + if ( ! hasImages ) { - return { mediaPlaceholder }; + return ( + + { innerBlocksProps.children } + { mediaPlaceholder } + + ); } const hasLinkTo = linkTo && linkTo !== 'none'; @@ -580,7 +593,7 @@ function GalleryEdit( props ) { ? mediaPlaceholder : undefined } - blockProps={ blockProps } + blockProps={ innerBlocksProps } insertBlocksAfter={ insertBlocksAfter } /> diff --git a/packages/block-library/src/gallery/gallery.js b/packages/block-library/src/gallery/gallery.js index e6176cc8a7256..957a141d51c6b 100644 --- a/packages/block-library/src/gallery/gallery.js +++ b/packages/block-library/src/gallery/gallery.js @@ -8,7 +8,6 @@ import classnames from 'classnames'; */ import { RichText, - useInnerBlocksProps, __experimentalGetElementClassName, } from '@wordpress/block-editor'; import { VisuallyHidden } from '@wordpress/components'; @@ -16,8 +15,6 @@ import { __ } from '@wordpress/i18n'; import { createBlock, getDefaultBlockName } from '@wordpress/blocks'; import { View } from '@wordpress/primitives'; -const allowedBlocks = [ 'core/image' ]; - export const Gallery = ( props ) => { const { attributes, @@ -31,16 +28,9 @@ export const Gallery = ( props ) => { const { align, columns, caption, imageCrop } = attributes; - const { children, ...innerBlocksProps } = useInnerBlocksProps( blockProps, { - allowedBlocks, - orientation: 'horizontal', - renderAppender: false, - __experimentalLayout: { type: 'default', alignments: [] }, - } ); - return (
{ } ) } > - { children } - { isSelected && ! children && ( + { blockProps.children } + { isSelected && ! blockProps.children && ( { mediaPlaceholder } diff --git a/packages/block-library/src/gallery/v1/edit.js b/packages/block-library/src/gallery/v1/edit.js index e79760a544cd2..9c36f02ec028a 100644 --- a/packages/block-library/src/gallery/v1/edit.js +++ b/packages/block-library/src/gallery/v1/edit.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { find, get, isEmpty, map } from 'lodash'; +import { get, isEmpty, map } from 'lodash'; /** * WordPress dependencies @@ -211,7 +211,7 @@ function GalleryEdit( props ) { // string, so ensure comparison works correctly by converting the // newImage.id to a string. const newImageId = newImage.id.toString(); - const currentImage = find( images, { id: newImageId } ); + const currentImage = images.find( ( { id } ) => id === newImageId ); const currentImageCaption = currentImage ? currentImage.caption : newImage.caption; @@ -220,9 +220,9 @@ function GalleryEdit( props ) { return currentImageCaption; } - const attachment = find( attachmentCaptions, { - id: newImageId, - } ); + const attachment = attachmentCaptions.find( + ( { id } ) => id === newImageId + ); // If the attachment caption is updated. if ( attachment && attachment.caption !== newImage.caption ) { diff --git a/packages/block-library/src/image/image.js b/packages/block-library/src/image/image.js index 37a8d25d1f9fa..5eb511b455864 100644 --- a/packages/block-library/src/image/image.js +++ b/packages/block-library/src/image/image.js @@ -611,6 +611,7 @@ export default function Image( { height: parseInt( currentHeight + delta.height, 10 ), } ); } } + resizeRatio={ align === 'center' ? 2 : 1 } > { img } diff --git a/packages/block-library/src/navigation-link/edit.js b/packages/block-library/src/navigation-link/edit.js index 02c96d2e5f61e..7e58527d947f5 100644 --- a/packages/block-library/src/navigation-link/edit.js +++ b/packages/block-library/src/navigation-link/edit.js @@ -424,6 +424,7 @@ export default function NavigationLinkEdit( { const ALLOWED_BLOCKS = [ 'core/navigation-link', 'core/navigation-submenu', + 'core/page-list', ]; const DEFAULT_BLOCK = { name: 'core/navigation-link', diff --git a/packages/block-library/src/navigation-link/index.php b/packages/block-library/src/navigation-link/index.php index 8853d49f6e2cf..36bfbf5d1fc0a 100644 --- a/packages/block-library/src/navigation-link/index.php +++ b/packages/block-library/src/navigation-link/index.php @@ -376,3 +376,35 @@ function gutenberg_disable_tabs_for_navigation_link_block( $settings ) { } add_filter( 'block_editor_settings_all', 'gutenberg_disable_tabs_for_navigation_link_block' ); + +/** + * Enables animation of the block inspector for the Navigation Link block. + * + * See: + * - https://github.com/WordPress/gutenberg/pull/46342 + * - https://github.com/WordPress/gutenberg/issues/45884 + * + * @param array $settings Default editor settings. + * @return array Filtered editor settings. + */ +function gutenberg_enable_animation_for_navigation_link_inspector( $settings ) { + $current_animation_settings = _wp_array_get( + $settings, + array( '__experimentalBlockInspectorAnimation' ), + array() + ); + + $settings['__experimentalBlockInspectorAnimation'] = array_merge( + $current_animation_settings, + array( + 'core/navigation-link' => + array( + 'enterDirection' => 'rightToLeft', + ), + ) + ); + + return $settings; +} + +add_filter( 'block_editor_settings_all', 'gutenberg_enable_animation_for_navigation_link_inspector' ); diff --git a/packages/block-library/src/navigation-link/link-ui.js b/packages/block-library/src/navigation-link/link-ui.js index 3c7387740e379..a2df4f2a29405 100644 --- a/packages/block-library/src/navigation-link/link-ui.js +++ b/packages/block-library/src/navigation-link/link-ui.js @@ -74,10 +74,12 @@ function LinkControlTransforms( { clientId } ) { const { replaceBlock } = useDispatch( blockEditorStore ); const featuredBlocks = [ + 'core/page-list', 'core/site-logo', 'core/social-links', 'core/search', ]; + const transforms = blockTransforms.filter( ( item ) => { return featuredBlocks.includes( item.name ); } ); diff --git a/packages/block-library/src/navigation-link/transforms.js b/packages/block-library/src/navigation-link/transforms.js index 7b213a4805106..fb450a13a02dc 100644 --- a/packages/block-library/src/navigation-link/transforms.js +++ b/packages/block-library/src/navigation-link/transforms.js @@ -40,6 +40,13 @@ const transforms = { return createBlock( 'core/navigation-link' ); }, }, + { + type: 'block', + blocks: [ 'core/page-list' ], + transform: () => { + return createBlock( 'core/navigation-link' ); + }, + }, ], to: [ { @@ -91,6 +98,13 @@ const transforms = { } ); }, }, + { + type: 'block', + blocks: [ 'core/page-list' ], + transform: () => { + return createBlock( 'core/page-list' ); + }, + }, ], }; diff --git a/packages/block-library/src/navigation-submenu/edit.js b/packages/block-library/src/navigation-submenu/edit.js index e4568c8d48e2d..47a222bd57bb6 100644 --- a/packages/block-library/src/navigation-submenu/edit.js +++ b/packages/block-library/src/navigation-submenu/edit.js @@ -43,7 +43,11 @@ import { name } from './block.json'; import { LinkUI } from '../navigation-link/link-ui'; import { updateAttributes } from '../navigation-link/update-attributes'; -const ALLOWED_BLOCKS = [ 'core/navigation-link', 'core/navigation-submenu' ]; +const ALLOWED_BLOCKS = [ + 'core/navigation-link', + 'core/navigation-submenu', + 'core/page-list', +]; const DEFAULT_BLOCK = { name: 'core/navigation-link', diff --git a/packages/block-library/src/navigation-submenu/index.php b/packages/block-library/src/navigation-submenu/index.php index 99c86493d5b31..c1c8006039f55 100644 --- a/packages/block-library/src/navigation-submenu/index.php +++ b/packages/block-library/src/navigation-submenu/index.php @@ -321,3 +321,35 @@ function gutenberg_disable_tabs_for_navigation_submenu_block( $settings ) { } add_filter( 'block_editor_settings_all', 'gutenberg_disable_tabs_for_navigation_submenu_block' ); + +/** + * Enables animation of the block inspector for the Navigation Submenu block. + * + * See: + * - https://github.com/WordPress/gutenberg/pull/46342 + * - https://github.com/WordPress/gutenberg/issues/45884 + * + * @param array $settings Default editor settings. + * @return array Filtered editor settings. + */ +function gutenberg_enable_animation_for_navigation_submenu_inspector( $settings ) { + $current_animation_settings = _wp_array_get( + $settings, + array( '__experimentalBlockInspectorAnimation' ), + array() + ); + + $settings['__experimentalBlockInspectorAnimation'] = array_merge( + $current_animation_settings, + array( + 'core/navigation-submenu' => + array( + 'enterDirection' => 'rightToLeft', + ), + ) + ); + + return $settings; +} + +add_filter( 'block_editor_settings_all', 'gutenberg_enable_animation_for_navigation_submenu_inspector' ); diff --git a/packages/block-library/src/navigation/edit/index.js b/packages/block-library/src/navigation/edit/index.js index b414409791cbf..d94e0022d15ea 100644 --- a/packages/block-library/src/navigation/edit/index.js +++ b/packages/block-library/src/navigation/edit/index.js @@ -717,13 +717,13 @@ function Navigation( { return ( { + const { __unstableGetClientIdsTree } = select( blockEditorStore ); + return __unstableGetClientIdsTree( clientId ); + }, + [ clientId ] + ); + return ( { isOffCanvasNavigationEditorEnabled ? ( diff --git a/packages/block-library/src/navigation/index.php b/packages/block-library/src/navigation/index.php index 030cab4501eed..971a5f500e774 100644 --- a/packages/block-library/src/navigation/index.php +++ b/packages/block-library/src/navigation/index.php @@ -856,3 +856,35 @@ function block_core_navigation_typographic_presets_backcompatibility( $parsed_bl } add_filter( 'render_block_data', 'block_core_navigation_typographic_presets_backcompatibility' ); + +/** + * Enables animation of the block inspector for the Navigation block. + * + * See: + * - https://github.com/WordPress/gutenberg/pull/46342 + * - https://github.com/WordPress/gutenberg/issues/45884 + * + * @param array $settings Default editor settings. + * @return array Filtered editor settings. + */ +function gutenberg_enable_animation_for_navigation_inspector( $settings ) { + $current_animation_settings = _wp_array_get( + $settings, + array( '__experimentalBlockInspectorAnimation' ), + array() + ); + + $settings['__experimentalBlockInspectorAnimation'] = array_merge( + $current_animation_settings, + array( + 'core/navigation' => + array( + 'enterDirection' => 'leftToRight', + ), + ) + ); + + return $settings; +} + +add_filter( 'block_editor_settings_all', 'gutenberg_enable_animation_for_navigation_inspector' ); diff --git a/packages/block-library/src/page-list/block.json b/packages/block-library/src/page-list/block.json index 2fc6993849d6f..e8ff316a9fb4f 100644 --- a/packages/block-library/src/page-list/block.json +++ b/packages/block-library/src/page-list/block.json @@ -30,7 +30,20 @@ ], "supports": { "reusable": false, - "html": false + "html": false, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalFontFamily": true, + "__experimentalFontWeight": true, + "__experimentalFontStyle": true, + "__experimentalTextTransform": true, + "__experimentalTextDecoration": true, + "__experimentalLetterSpacing": true, + "__experimentalDefaultControls": { + "fontSize": true + } + } }, "editorStyle": "wp-block-page-list-editor", "style": "wp-block-page-list" diff --git a/packages/block-library/src/page-list/edit.js b/packages/block-library/src/page-list/edit.js index b3d2db31d5e04..7372191723f46 100644 --- a/packages/block-library/src/page-list/edit.js +++ b/packages/block-library/src/page-list/edit.js @@ -25,7 +25,7 @@ import { Button, } from '@wordpress/components'; import { __, sprintf } from '@wordpress/i18n'; -import { useMemo, useState, useEffect } from '@wordpress/element'; +import { useMemo, useState } from '@wordpress/element'; import { useEntityRecords } from '@wordpress/core-data'; import { useSelect, useDispatch } from '@wordpress/data'; @@ -39,6 +39,7 @@ import { convertDescription } from './constants'; // We only show the edit option when page count is <= MAX_PAGE_COUNT // Performance of Navigation Links is not good past this value. const MAX_PAGE_COUNT = 100; +const NOOP = () => {}; export default function PageListEdit( { context, @@ -49,8 +50,6 @@ export default function PageListEdit( { const { parentPageID } = attributes; const [ pages ] = useGetPages(); const { pagesByParentId, totalPages, hasResolvedPages } = usePageData(); - const { replaceInnerBlocks, __unstableMarkNextChangeAsNotPersistent } = - useDispatch( blockEditorStore ); const isNavigationChild = 'showSubmenuIcon' in context; const allowConvertToLinks = @@ -133,6 +132,9 @@ export default function PageListEdit( { renderAppender: false, __unstableDisableDropZone: true, templateLock: 'all', + onInput: NOOP, + onChange: NOOP, + value: blockList, } ); const getBlockContent = () => { @@ -185,13 +187,6 @@ export default function PageListEdit( { } }; - useEffect( () => { - __unstableMarkNextChangeAsNotPersistent(); - if ( blockList ) { - replaceInnerBlocks( clientId, blockList ); - } - }, [ clientId, blockList ] ); - const { replaceBlock, selectBlock } = useDispatch( blockEditorStore ); const { parentNavBlockClientId } = useSelect( ( select ) => { diff --git a/packages/block-library/src/page-list/index.js b/packages/block-library/src/page-list/index.js index 7e13c23f229f2..995e3a1c7f97a 100644 --- a/packages/block-library/src/page-list/index.js +++ b/packages/block-library/src/page-list/index.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { pages as icon } from '@wordpress/icons'; +import { pages, update } from '@wordpress/icons'; /** * Internal dependencies @@ -15,7 +15,13 @@ const { name } = metadata; export { metadata, name }; export const settings = { - icon, + icon: ( { context } ) => { + if ( context === 'list-view' ) { + return update; + } + + return pages; + }, example: {}, edit, }; diff --git a/packages/block-library/src/post-template/block.json b/packages/block-library/src/post-template/block.json index 380b6d55f71fa..bc9910b47d1dc 100644 --- a/packages/block-library/src/post-template/block.json +++ b/packages/block-library/src/post-template/block.json @@ -22,6 +22,14 @@ "__experimentalLayout": { "allowEditing": false }, + "color": { + "gradients": true, + "link": true, + "__experimentalDefaultControls": { + "background": true, + "text": true + } + }, "typography": { "fontSize": true, "lineHeight": true, diff --git a/packages/block-library/src/query/block.json b/packages/block-library/src/query/block.json index cd09e22ee57f7..1974761962ec9 100644 --- a/packages/block-library/src/query/block.json +++ b/packages/block-library/src/query/block.json @@ -50,14 +50,6 @@ "supports": { "align": [ "wide", "full" ], "html": false, - "color": { - "gradients": true, - "link": true, - "__experimentalDefaultControls": { - "background": true, - "text": true - } - }, "__experimentalLayout": true }, "editorStyle": "wp-block-query-editor" diff --git a/packages/block-library/src/query/deprecated.js b/packages/block-library/src/query/deprecated.js index 42ac3dc4f1c49..b23455d50489b 100644 --- a/packages/block-library/src/query/deprecated.js +++ b/packages/block-library/src/query/deprecated.js @@ -1,12 +1,18 @@ /** * WordPress dependencies */ +import { createBlock } from '@wordpress/blocks'; import { InnerBlocks, useInnerBlocksProps, useBlockProps, } from '@wordpress/block-editor'; +/** + * Internal dependencies + */ +import cleanEmptyObject from '../utils/clean-empty-object'; + const migrateToTaxQuery = ( attributes ) => { const { query } = attributes; const { categoryIds, tagIds, ...newQuery } = query; @@ -25,106 +31,271 @@ const migrateToTaxQuery = ( attributes ) => { }; }; -const deprecated = [ - // Version with `categoryIds and tagIds`. - { - attributes: { - queryId: { - type: 'number', - }, - query: { - type: 'object', - default: { - perPage: null, - pages: 0, - offset: 0, - postType: 'post', - categoryIds: [], - tagIds: [], - order: 'desc', - orderBy: 'date', - author: '', - search: '', - exclude: [], - sticky: '', - inherit: true, - }, - }, - tagName: { - type: 'string', - default: 'div', +const migrateColors = ( attributes, innerBlocks ) => { + // Remove color style attributes from the Query block. + const { style, backgroundColor, gradient, textColor, ...newAttributes } = + attributes; + + const hasColorStyles = + backgroundColor || + gradient || + textColor || + style?.color || + style?.elements?.link; + + // If the query block doesn't currently have any color styles, + // nothing needs migrating. + if ( ! hasColorStyles ) { + return [ attributes, innerBlocks ]; + } + + // Clean color values from style attribute object. + if ( style ) { + newAttributes.style = cleanEmptyObject( { + ...style, + color: undefined, + elements: { + ...style.elements, + link: undefined, }, - displayLayout: { - type: 'object', - default: { - type: 'list', - }, + } ); + } + + // If the inner blocks are already wrapped in a single group + // block, add the color support styles to that group block. + if ( hasSingleInnerGroupBlock( innerBlocks ) ) { + const groupBlock = innerBlocks[ 0 ]; + + // Create new styles for the group block. + const hasStyles = + style?.color || + style?.elements?.link || + groupBlock.attributes.style; + + const newStyles = hasStyles + ? cleanEmptyObject( { + ...groupBlock.attributes.style, + color: style?.color, + elements: style?.elements?.link + ? { link: style?.elements?.link } + : undefined, + } ) + : undefined; + + // Create a new Group block from the original. + const updatedGroupBlock = createBlock( + 'core/group', + { + ...groupBlock.attributes, + backgroundColor, + gradient, + textColor, + style: newStyles, }, + groupBlock.innerBlocks + ); + + return [ newAttributes, [ updatedGroupBlock ] ]; + } + + // When we don't have a single wrapping group block for the inner + // blocks, wrap the current inner blocks in a group applying the + // color styles to that. + const newGroupBlock = createBlock( + 'core/group', + { + backgroundColor, + gradient, + textColor, + style: cleanEmptyObject( { + color: style?.color, + elements: style?.elements?.link + ? { link: style?.elements?.link } + : undefined, + } ), + }, + innerBlocks + ); + + return [ newAttributes, [ newGroupBlock ] ]; +}; + +const hasSingleInnerGroupBlock = ( innerBlocks = [] ) => + innerBlocks.length === 1 && innerBlocks[ 0 ].name === 'core/group'; + +// Version with NO wrapper `div` element. +const v1 = { + attributes: { + queryId: { + type: 'number', }, - supports: { - align: [ 'wide', 'full' ], - html: false, - color: { - gradients: true, - link: true, + query: { + type: 'object', + default: { + perPage: null, + pages: 0, + offset: 0, + postType: 'post', + categoryIds: [], + tagIds: [], + order: 'desc', + orderBy: 'date', + author: '', + search: '', + exclude: [], + sticky: '', + inherit: true, }, - __experimentalLayout: true, }, - isEligible: ( { query: { categoryIds, tagIds } = {} } ) => - categoryIds || tagIds, - migrate: migrateToTaxQuery, - save( { attributes: { tagName: Tag = 'div' } } ) { - const blockProps = useBlockProps.save(); - const innerBlocksProps = useInnerBlocksProps.save( blockProps ); - return ; + layout: { + type: 'object', + default: { + type: 'list', + }, }, }, - // Version with NO wrapper `div` element. - { - attributes: { - queryId: { - type: 'number', + supports: { + html: false, + }, + migrate( attributes ) { + const withTaxQuery = migrateToTaxQuery( attributes ); + const { layout, ...restWithTaxQuery } = withTaxQuery; + return { + ...restWithTaxQuery, + displayLayout: withTaxQuery.layout, + }; + }, + save() { + return ; + }, +}; + +// Version with `categoryIds and tagIds`. +const v2 = { + attributes: { + queryId: { + type: 'number', + }, + query: { + type: 'object', + default: { + perPage: null, + pages: 0, + offset: 0, + postType: 'post', + categoryIds: [], + tagIds: [], + order: 'desc', + orderBy: 'date', + author: '', + search: '', + exclude: [], + sticky: '', + inherit: true, }, - query: { - type: 'object', - default: { - perPage: null, - pages: 0, - offset: 0, - postType: 'post', - categoryIds: [], - tagIds: [], - order: 'desc', - orderBy: 'date', - author: '', - search: '', - exclude: [], - sticky: '', - inherit: true, - }, + }, + tagName: { + type: 'string', + default: 'div', + }, + displayLayout: { + type: 'object', + default: { + type: 'list', }, - layout: { - type: 'object', - default: { - type: 'list', - }, + }, + }, + supports: { + align: [ 'wide', 'full' ], + html: false, + color: { + gradients: true, + link: true, + }, + __experimentalLayout: true, + }, + isEligible: ( { query: { categoryIds, tagIds } = {} } ) => + categoryIds || tagIds, + migrate( attributes, innerBlocks ) { + const withTaxQuery = migrateToTaxQuery( attributes ); + return migrateColors( withTaxQuery, innerBlocks ); + }, + save( { attributes: { tagName: Tag = 'div' } } ) { + const blockProps = useBlockProps.save(); + const innerBlocksProps = useInnerBlocksProps.save( blockProps ); + return ; + }, +}; + +// Version with color support prior to moving it to the PostTemplate block. +const v3 = { + attributes: { + queryId: { + type: 'number', + }, + query: { + type: 'object', + default: { + perPage: null, + pages: 0, + offset: 0, + postType: 'post', + order: 'desc', + orderBy: 'date', + author: '', + search: '', + exclude: [], + sticky: '', + inherit: true, + taxQuery: null, + parents: [], }, }, - supports: { - html: false, + tagName: { + type: 'string', + default: 'div', + }, + displayLayout: { + type: 'object', + default: { + type: 'list', + }, }, - migrate( attributes ) { - const withTaxQuery = migrateToTaxQuery( attributes ); - const { layout, ...restWithTaxQuery } = withTaxQuery; - return { - ...restWithTaxQuery, - displayLayout: withTaxQuery.layout, - }; + namespace: { + type: 'string', }, - save() { - return ; + }, + supports: { + align: [ 'wide', 'full' ], + html: false, + color: { + gradients: true, + link: true, + __experimentalDefaultControls: { + background: true, + text: true, + }, }, + __experimentalLayout: true, + }, + isEligible( attributes ) { + const { style, backgroundColor, gradient, textColor } = attributes; + return ( + backgroundColor || + gradient || + textColor || + style?.color || + style?.elements?.link + ); }, -]; + migrate: migrateColors, + save( { attributes: { tagName: Tag = 'div' } } ) { + const blockProps = useBlockProps.save(); + const innerBlocksProps = useInnerBlocksProps.save( blockProps ); + return ; + }, +}; + +const deprecated = [ v3, v2, v1 ]; export default deprecated; diff --git a/packages/block-library/src/search/edit.js b/packages/block-library/src/search/edit.js index a3db69287f8f7..78ff685ff01fe 100644 --- a/packages/block-library/src/search/edit.js +++ b/packages/block-library/src/search/edit.js @@ -114,10 +114,10 @@ export default function SearchEdit( { } const colorProps = useColorProps( attributes ); - const fluidTypographyEnabled = useSetting( 'typography.fluid' ); + const fluidTypographySettings = useSetting( 'typography.fluid' ); const typographyProps = useTypographyProps( attributes, - fluidTypographyEnabled + fluidTypographySettings ); const unitControlInstanceId = useInstanceId( UnitControl ); const unitControlInputId = `wp-block-search__width-${ unitControlInstanceId }`; diff --git a/packages/block-library/src/social-link/social-list.js b/packages/block-library/src/social-link/social-list.js index 9ac527dccd0d3..38c1ef91f9938 100644 --- a/packages/block-library/src/social-link/social-list.js +++ b/packages/block-library/src/social-link/social-list.js @@ -1,8 +1,3 @@ -/** - * External dependencies - */ -import { find } from 'lodash'; - /** * WordPress dependencies */ @@ -22,7 +17,7 @@ import { ChainIcon } from './icons'; * @return {WPComponent} Icon component for social service. */ export const getIconBySite = ( name ) => { - const variation = find( variations, { name } ); + const variation = variations.find( ( v ) => v.name === name ); return variation ? variation.icon : ChainIcon; }; @@ -34,6 +29,6 @@ export const getIconBySite = ( name ) => { * @return {string} Display name for social service */ export const getNameBySite = ( name ) => { - const variation = find( variations, { name } ); + const variation = variations.find( ( v ) => v.name === name ); return variation ? variation.title : __( 'Social Icon' ); }; diff --git a/packages/block-library/src/template-part/edit/advanced-controls.js b/packages/block-library/src/template-part/edit/advanced-controls.js index d57d6b2f63180..ca42726b5655f 100644 --- a/packages/block-library/src/template-part/edit/advanced-controls.js +++ b/packages/block-library/src/template-part/edit/advanced-controls.js @@ -7,12 +7,18 @@ import { sprintf, __ } from '@wordpress/i18n'; import { InspectorControls } from '@wordpress/block-editor'; import { useSelect } from '@wordpress/data'; +/** + * Internal dependencies + */ +import { TemplatePartImportControls } from './import-controls'; + export function TemplatePartAdvancedControls( { tagName, setAttributes, isEntityAvailable, templatePartId, defaultWrapper, + hasInnerBlocks, } ) { const [ area, setArea ] = useEntityProp( 'postType', @@ -87,6 +93,12 @@ export function TemplatePartAdvancedControls( { value={ tagName || '' } onChange={ ( value ) => setAttributes( { tagName: value } ) } /> + { ! hasInnerBlocks && ( + + ) } ); } diff --git a/packages/block-library/src/template-part/edit/import-controls.js b/packages/block-library/src/template-part/edit/import-controls.js new file mode 100644 index 0000000000000..1512cd936f02c --- /dev/null +++ b/packages/block-library/src/template-part/edit/import-controls.js @@ -0,0 +1,180 @@ +/** + * WordPress dependencies + */ +import { __, sprintf } from '@wordpress/i18n'; +import { useMemo, useState } from '@wordpress/element'; +import { useDispatch, useSelect, useRegistry } from '@wordpress/data'; +import { + Button, + FlexBlock, + FlexItem, + SelectControl, + __experimentalHStack as HStack, + __experimentalSpacer as Spacer, +} from '@wordpress/components'; +import { + switchToBlockType, + getPossibleBlockTransformations, +} from '@wordpress/blocks'; +import { store as coreStore } from '@wordpress/core-data'; +import { store as noticesStore } from '@wordpress/notices'; + +/** + * Internal dependencies + */ +import { useCreateTemplatePartFromBlocks } from './utils/hooks'; +import { transformWidgetToBlock } from './utils/transformers'; + +export function TemplatePartImportControls( { area, setAttributes } ) { + const [ selectedSidebar, setSelectedSidebar ] = useState( '' ); + const [ isBusy, setIsBusy ] = useState( false ); + + const registry = useRegistry(); + const sidebars = useSelect( ( select ) => { + return select( coreStore ).getSidebars( { + per_page: -1, + _fields: 'id,name,description,status,widgets', + } ); + }, [] ); + const { createErrorNotice } = useDispatch( noticesStore ); + + const createFromBlocks = useCreateTemplatePartFromBlocks( + area, + setAttributes + ); + + const options = useMemo( () => { + const sidebarOptions = ( sidebars ?? [] ) + .filter( + ( widgetArea ) => + widgetArea.id !== 'wp_inactive_widgets' && + widgetArea.widgets.length > 0 + ) + .map( ( widgetArea ) => { + return { + value: widgetArea.id, + label: widgetArea.name, + }; + } ); + + if ( ! sidebarOptions.length ) { + return []; + } + + return [ + { value: '', label: __( 'Select widget area' ) }, + ...sidebarOptions, + ]; + }, [ sidebars ] ); + + async function createFromWidgets( event ) { + event.preventDefault(); + + if ( isBusy || ! selectedSidebar ) { + return; + } + + setIsBusy( true ); + + const sidebar = options.find( + ( { value } ) => value === selectedSidebar + ); + const { getWidgets } = registry.resolveSelect( coreStore ); + + // The widgets API always returns a successful response. + const widgets = await getWidgets( { + sidebar: sidebar.value, + _embed: 'about', + } ); + + const skippedWidgets = new Set(); + const blocks = widgets.flatMap( ( widget ) => { + const block = transformWidgetToBlock( widget ); + + if ( block.name !== 'core/legacy-widget' ) { + return block; + } + + const transforms = getPossibleBlockTransformations( [ + block, + ] ).filter( ( item ) => { + // The block without any transformations can't be a wildcard. + if ( ! item.transforms ) { + return true; + } + + const hasWildCardFrom = item.transforms?.from?.find( + ( from ) => from.blocks && from.blocks.includes( '*' ) + ); + const hasWildCardTo = item.transforms?.to?.find( + ( to ) => to.blocks && to.blocks.includes( '*' ) + ); + + return ! hasWildCardFrom && ! hasWildCardTo; + } ); + + // Skip the block if we have no matching transformations. + if ( ! transforms.length ) { + skippedWidgets.add( widget.id_base ); + return []; + } + + // Try transforming the Legacy Widget into a first matching block. + return switchToBlockType( block, transforms[ 0 ].name ); + } ); + + await createFromBlocks( + blocks, + /* translators: %s: name of the widget area */ + sprintf( __( 'Widget area: %s' ), sidebar.label ) + ); + + if ( skippedWidgets.size ) { + createErrorNotice( + sprintf( + /* translators: %s: the list of widgets */ + __( 'Unable to import the following widgets: %s.' ), + Array.from( skippedWidgets ).join( ', ' ) + ), + { + type: 'snackbar', + } + ); + } + + setIsBusy( false ); + } + + return ( + + + + setSelectedSidebar( value ) } + disabled={ ! options.length } + __next36pxDefaultSize + __nextHasNoMarginBottom + /> + + + + + + + ); +} diff --git a/packages/block-library/src/template-part/edit/index.js b/packages/block-library/src/template-part/edit/index.js index 83b1a8d450b6c..502bd3d00feef 100644 --- a/packages/block-library/src/template-part/edit/index.js +++ b/packages/block-library/src/template-part/edit/index.js @@ -141,6 +141,7 @@ export default function TemplatePartEdit( { isEntityAvailable={ isEntityAvailable } templatePartId={ templatePartId } defaultWrapper={ areaObject.tagName } + hasInnerBlocks={ innerBlocks.length > 0 } /> { isPlaceholder && ( diff --git a/packages/block-library/src/template-part/edit/utils/hooks.js b/packages/block-library/src/template-part/edit/utils/hooks.js index e5b60131d84ee..37eb762a24144 100644 --- a/packages/block-library/src/template-part/edit/utils/hooks.js +++ b/packages/block-library/src/template-part/edit/utils/hooks.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { find, kebabCase } from 'lodash'; +import { kebabCase } from 'lodash'; /** * WordPress dependencies @@ -150,8 +150,12 @@ export function useTemplatePartArea( area ) { ).__experimentalGetDefaultTemplatePartAreas(); /* eslint-enable @wordpress/data-no-store-string-literals */ - const selectedArea = find( definedAreas, { area } ); - const defaultArea = find( definedAreas, { area: 'uncategorized' } ); + const selectedArea = definedAreas.find( + ( definedArea ) => definedArea.area === area + ); + const defaultArea = definedAreas.find( + ( definedArea ) => definedArea.area === 'uncategorized' + ); return { icon: selectedArea?.icon || defaultArea?.icon, diff --git a/packages/block-library/src/template-part/edit/utils/transformers.js b/packages/block-library/src/template-part/edit/utils/transformers.js new file mode 100644 index 0000000000000..fdef84d785b90 --- /dev/null +++ b/packages/block-library/src/template-part/edit/utils/transformers.js @@ -0,0 +1,37 @@ +/** + * WordPress dependencies + */ +import { createBlock, parse } from '@wordpress/blocks'; + +/** + * Converts a widget entity record into a block. + * + * @param {Object} widget The widget entity record. + * @return {Object} a block (converted from the entity record). + */ +export function transformWidgetToBlock( widget ) { + if ( widget.id_base === 'block' ) { + const parsedBlocks = parse( widget.instance.raw.content, { + __unstableSkipAutop: true, + } ); + if ( ! parsedBlocks.length ) { + return createBlock( 'core/paragraph', {}, [] ); + } + + return parsedBlocks[ 0 ]; + } + + let attributes; + if ( widget._embedded.about[ 0 ].is_multi ) { + attributes = { + idBase: widget.id_base, + instance: widget.instance, + }; + } else { + attributes = { + id: widget.id, + }; + } + + return createBlock( 'core/legacy-widget', attributes, [] ); +} diff --git a/packages/blocks/src/api/parser/get-block-attributes.js b/packages/blocks/src/api/parser/get-block-attributes.js index c8d7aa88204cc..ffa67c11250c2 100644 --- a/packages/blocks/src/api/parser/get-block-attributes.js +++ b/packages/blocks/src/api/parser/get-block-attributes.js @@ -102,11 +102,11 @@ export function isOfTypes( value, types ) { * commentAttributes returns the attribute value depending on its source * definition of the given attribute key. * - * @param {string} attributeKey Attribute key. - * @param {Object} attributeSchema Attribute's schema. - * @param {Node} innerDOM Parsed DOM of block's inner HTML. - * @param {Object} commentAttributes Block's comment attributes. - * @param {string} innerHTML Raw HTML from block node's innerHTML property. + * @param {string} attributeKey Attribute key. + * @param {Object} attributeSchema Attribute's schema. + * @param {Node} innerDOM Parsed DOM of block's inner HTML. + * @param {Object} commentAttributes Block's comment attributes. + * @param {string} innerHTML Raw HTML from block node's innerHTML property. * * @return {*} Attribute value. */ diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index bcb1b61026718..f459061f109e1 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -16,6 +16,7 @@ - `ColorPalette`: show "Clear" button even when colors array is empty ([#46001](https://github.com/WordPress/gutenberg/pull/46001)). - `InputControl`: Fix internal `Flex` wrapper usage that could add an unintended `height: 100%` ([#46213](https://github.com/WordPress/gutenberg/pull/46213)). - `Navigator`: Allow calling `goTo` and `goBack` twice in one render cycle ([#46391](https://github.com/WordPress/gutenberg/pull/46391)). +- `Modal`: Fix unexpected modal closing in IME Composition ([#46453](https://github.com/WordPress/gutenberg/pull/46453)). ### Enhancements @@ -28,6 +29,8 @@ - `Popover`: Prevent unnecessary paint caused by using outline ([#46201](https://github.com/WordPress/gutenberg/pull/46201)). - `PaletteEdit`: Global styles: add onChange actions to color palette items [#45681](https://github.com/WordPress/gutenberg/pull/45681). - Lighten the border color on control components ([#46252](https://github.com/WordPress/gutenberg/pull/46252)). +- `Popover`: Prevent unnecessary paint when scrolling by using transform instead of top/left positionning ([#46187](https://github.com/WordPress/gutenberg/pull/46187)). +- `CircularOptionPicker`: Prevent unecessary paint on hover ([#46197](https://github.com/WordPress/gutenberg/pull/46197)). ### Experimental @@ -42,6 +45,10 @@ - `Dashicon`: Convert to TypeScript ([#45924](https://github.com/WordPress/gutenberg/pull/45924)). - `PaletteEdit`: add follow up changelog for #45681 and tests [#46095](https://github.com/WordPress/gutenberg/pull/46095). - `AlignmentMatrixControl`: Convert to TypeScript ([#46162](https://github.com/WordPress/gutenberg/pull/46162)). +- `Autocomplete`: Refactor away from `_.find()` ([#46537](https://github.com/WordPress/gutenberg/pull/46537)). +- `TabPanel`: Refactor away from `_.find()` ([#46537](https://github.com/WordPress/gutenberg/pull/46537)). +- `BottomSheetPickerCell`: Refactor away from `_.find()` for mobile ([#46537](https://github.com/WordPress/gutenberg/pull/46537)). +- Refactor global styles context away from `_.find()` for mobile ([#46537](https://github.com/WordPress/gutenberg/pull/46537)). ### Documentation diff --git a/packages/components/src/autocomplete/index.js b/packages/components/src/autocomplete/index.js index 45f5415e55de0..13c27d49b81b1 100644 --- a/packages/components/src/autocomplete/index.js +++ b/packages/components/src/autocomplete/index.js @@ -1,7 +1,6 @@ /** * External dependencies */ -import { find } from 'lodash'; import removeAccents from 'remove-accents'; /** @@ -290,8 +289,7 @@ function useAutocomplete( { const textAfterSelection = getTextContent( slice( record, undefined, getTextContent( record ).length ) ); - const completer = find( - completers, + const completer = completers?.find( ( { triggerPrefix, allowContext } ) => { const index = text.lastIndexOf( triggerPrefix ); diff --git a/packages/components/src/higher-order/navigate-regions/style.scss b/packages/components/src/higher-order/navigate-regions/style.scss index 807dd0e5dcae5..6e66c854dd70e 100644 --- a/packages/components/src/higher-order/navigate-regions/style.scss +++ b/packages/components/src/higher-order/navigate-regions/style.scss @@ -4,14 +4,17 @@ } .is-focusing-regions { - [role="region"]:focus { + [role="region"]:focus::after { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + content: ""; + pointer-events: none; outline: 4px solid $components-color-accent; outline-offset: -4px; - - .interface-navigable-region__stacker { - position: relative; - z-index: z-index(".is-focusing-regions [role='region']:focus .interface-navigable-region__stacker"); - } + z-index: z-index(".is-focusing-regions {region} :focus::after"); } // Fixes for edge cases. @@ -25,40 +28,11 @@ // regardles of the CSS used on other components. // Header top bar when Distraction free mode is on. - &.is-distraction-free .interface-interface-skeleton__header { - .interface-navigable-region__stacker, - .edit-post-header { - outline: inherit; - outline-offset: inherit; - } - } - - // Sidebar toggle button shown when navigating regions. - .interface-interface-skeleton__sidebar { - .interface-navigable-region__stacker, - .edit-post-layout__toggle-sidebar-panel { - outline: inherit; - outline-offset: inherit; - } - } - - // Publish sidebar toggle button shown when navigating regions. - .interface-interface-skeleton__actions { - .interface-navigable-region__stacker, - .edit-post-layout__toggle-publish-panel { - outline: inherit; - outline-offset: inherit; - } - } - - // Publish sidebar. - [role="region"].interface-interface-skeleton__actions:focus .editor-post-publish-panel { + &.is-distraction-free .interface-interface-skeleton__header .edit-post-header, + .interface-interface-skeleton__sidebar .edit-post-layout__toggle-sidebar-panel, + .interface-interface-skeleton__actions .edit-post-layout__toggle-publish-panel, + .editor-post-publish-panel { outline: 4px solid $components-color-accent; outline-offset: -4px; } } - -.interface-navigable-region__stacker { - height: 100%; - width: 100%; -} diff --git a/packages/components/src/mobile/bottom-sheet/picker-cell.native.js b/packages/components/src/mobile/bottom-sheet/picker-cell.native.js index 42c59cd025adf..2dbbdd08d4609 100644 --- a/packages/components/src/mobile/bottom-sheet/picker-cell.native.js +++ b/packages/components/src/mobile/bottom-sheet/picker-cell.native.js @@ -1,8 +1,3 @@ -/** - * External dependencies - */ -import { find } from 'lodash'; - /** * Internal dependencies */ @@ -23,7 +18,7 @@ export default function BottomSheetPickerCell( props ) { onChangeValue( newValue ); }; - const option = find( options, { value } ); + const option = options.find( ( opt ) => opt.value === value ); const label = option ? option.label : value; return ( diff --git a/packages/components/src/mobile/global-styles-context/utils.native.js b/packages/components/src/mobile/global-styles-context/utils.native.js index 7d985a9cccb6f..38536ec3f107f 100644 --- a/packages/components/src/mobile/global-styles-context/utils.native.js +++ b/packages/components/src/mobile/global-styles-context/utils.native.js @@ -2,7 +2,7 @@ * External dependencies */ import { camelCase } from 'change-case'; -import { find, get } from 'lodash'; +import { get } from 'lodash'; import { Dimensions } from 'react-native'; /** @@ -113,9 +113,9 @@ export function getBlockColors( } if ( ! isCustomColor ) { - const mappedColor = find( defaultColors, { - slug: value, - } ); + const mappedColor = Object.values( defaultColors ?? {} ).find( + ( { slug } ) => slug === value + ); if ( mappedColor ) { blockStyles[ styleKey ] = mappedColor.color; @@ -143,6 +143,7 @@ export function getBlockTypography( const typographyStyles = {}; const customBlockStyles = blockStyleAttributes?.style?.typography || {}; const blockGlobalStyles = baseGlobalStyles?.blocks?.[ blockName ]; + const parsedFontSizes = Object.values( fontSizes ?? {} ); // Global styles. if ( blockGlobalStyles?.typography ) { @@ -153,9 +154,9 @@ export function getBlockTypography( if ( parseInt( fontSize, 10 ) ) { typographyStyles.fontSize = fontSize; } else { - const mappedFontSize = find( fontSizes, { - slug: fontSize, - } ); + const mappedFontSize = parsedFontSizes.find( + ( { slug } ) => slug === fontSize + ); if ( mappedFontSize ) { typographyStyles.fontSize = mappedFontSize?.size; @@ -169,9 +170,9 @@ export function getBlockTypography( } if ( blockStyleAttributes?.fontSize && baseGlobalStyles ) { - const mappedFontSize = find( fontSizes, { - slug: blockStyleAttributes?.fontSize, - } ); + const mappedFontSize = parsedFontSizes.find( + ( { slug } ) => slug === blockStyleAttributes?.fontSize + ); if ( mappedFontSize ) { typographyStyles.fontSize = mappedFontSize?.size; @@ -212,9 +213,9 @@ export function parseStylesVariables( styles, mappedValues, customValues ) { const path = $2.split( '--' ); const mappedPresetValue = mappedValues[ path[ 0 ] ]; if ( mappedPresetValue && mappedPresetValue.slug ) { - const matchedValue = find( mappedPresetValue.values, { - slug: path[ 1 ], - } ); + const matchedValue = Object.values( + mappedPresetValue.values ?? {} + ).find( ( { slug } ) => slug === path[ 1 ] ); return matchedValue?.[ mappedPresetValue.slug ]; } return UNKNOWN_VALUE; @@ -244,9 +245,9 @@ export function parseStylesVariables( styles, mappedValues, customValues ) { if ( variable === 'var' ) { stylesBase = stylesBase.replace( varRegex, ( _$1, $2 ) => { if ( mappedValues?.color ) { - const matchedValue = find( mappedValues.color?.values, { - slug: $2, - } ); + const matchedValue = mappedValues.color?.values?.find( + ( { slug } ) => slug === $2 + ); return `"${ matchedValue?.color }"`; } return UNKNOWN_VALUE; diff --git a/packages/components/src/modal/index.tsx b/packages/components/src/modal/index.tsx index 8c4f3a8535704..10316b56993c4 100644 --- a/packages/components/src/modal/index.tsx +++ b/packages/components/src/modal/index.tsx @@ -98,6 +98,17 @@ function UnforwardedModal( }, [ bodyOpenClassName ] ); function handleEscapeKeyDown( event: KeyboardEvent< HTMLDivElement > ) { + if ( + // Ignore keydowns from IMEs + event.nativeEvent.isComposing || + // Workaround for Mac Safari where the final Enter/Backspace of an IME composition + // is `isComposing=false`, even though it's technically still part of the composition. + // These can only be detected by keyCode. + event.keyCode === 229 + ) { + return; + } + if ( shouldCloseOnEsc && event.code === 'Escape' && diff --git a/packages/components/src/modal/stories/index.tsx b/packages/components/src/modal/stories/index.tsx index 42b7874b1eeaa..7f414d47c2d11 100644 --- a/packages/components/src/modal/stories/index.tsx +++ b/packages/components/src/modal/stories/index.tsx @@ -12,6 +12,7 @@ import { useState } from '@wordpress/element'; * Internal dependencies */ import Button from '../../button'; +import InputControl from '../../input-control'; import Modal from '../'; import type { ModalProps } from '../types'; @@ -75,6 +76,8 @@ const Template: ComponentStory< typeof Modal > = ( { anim id est laborum.

+ + diff --git a/packages/components/src/modal/test/index.tsx b/packages/components/src/modal/test/index.tsx index a0778bfb8aab8..310dc3d6e63a9 100644 --- a/packages/components/src/modal/test/index.tsx +++ b/packages/components/src/modal/test/index.tsx @@ -2,6 +2,7 @@ * External dependencies */ import { render, screen, within } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; /** * Internal dependencies @@ -69,4 +70,18 @@ describe( 'Modal', () => { const title = within( dialog ).queryByText( 'Test Title' ); expect( title ).not.toBeInTheDocument(); } ); + + it( 'should call onRequestClose when the escape key is pressed', async () => { + const user = userEvent.setup( { + advanceTimers: jest.advanceTimersByTime, + } ); + const onRequestClose = jest.fn(); + render( + +

Modal content

+
+ ); + await user.keyboard( '[Escape]' ); + expect( onRequestClose ).toHaveBeenCalled(); + } ); } ); diff --git a/packages/components/src/popover/index.tsx b/packages/components/src/popover/index.tsx index c56d1f8475315..14f1ddd8bab96 100644 --- a/packages/components/src/popover/index.tsx +++ b/packages/components/src/popover/index.tsx @@ -489,8 +489,15 @@ const UnforwardedPopover = ( ? undefined : { position: strategy, - left: Number.isNaN( x ) ? 0 : x ?? undefined, - top: Number.isNaN( y ) ? 0 : y ?? undefined, + top: 0, + left: 0, + // `x` and `y` are framer-motion specific props and are shorthands + // for `translateX` and `translateY`. Currently it is not possible + // to use `translateX` and `translateY` because those values would + // be overridden by the return value of the + // `placementToMotionAnimationProps` function in `AnimatedWrapper` + x: Math.round( x ?? 0 ) || undefined, + y: Math.round( y ?? 0 ) || undefined, } } > diff --git a/packages/components/src/popover/style.scss b/packages/components/src/popover/style.scss index d595c23ae274a..790b840e6a648 100644 --- a/packages/components/src/popover/style.scss +++ b/packages/components/src/popover/style.scss @@ -7,6 +7,7 @@ $shadow-popover-border-top-only-alternate: 0 #{-$border-width} 0 $gray-900; .components-popover { z-index: z-index(".components-popover"); + will-change: transform; &.is-expanded { position: fixed; diff --git a/packages/components/src/range-control/test/index.tsx b/packages/components/src/range-control/test/index.tsx index 0343b084daaa4..bbb97dbfaa085 100644 --- a/packages/components/src/range-control/test/index.tsx +++ b/packages/components/src/range-control/test/index.tsx @@ -43,11 +43,11 @@ describe( 'RangeControl', () => { /> ); - // eslint-disable-next-line testing-library/no-container + // eslint-disable-next-line testing-library/no-container, testing-library/no-node-access const beforeIcon = container.querySelector( '.dashicons-format-image' ); - // eslint-disable-next-line testing-library/no-container + // eslint-disable-next-line testing-library/no-container, testing-library/no-node-access const afterIcon = container.querySelector( '.dashicons-format-video' ); diff --git a/packages/components/src/tab-panel/index.tsx b/packages/components/src/tab-panel/index.tsx index fce688089a55c..2504f7f214fa6 100644 --- a/packages/components/src/tab-panel/index.tsx +++ b/packages/components/src/tab-panel/index.tsx @@ -2,7 +2,6 @@ * External dependencies */ import classnames from 'classnames'; -import { find } from 'lodash'; /** * WordPress dependencies @@ -102,7 +101,7 @@ export function TabPanel( { ) => { child.click(); }; - const selectedTab = find( tabs, { name: selected } ); + const selectedTab = tabs.find( ( { name } ) => name === selected ); const selectedId = `${ instanceId }-${ selectedTab?.name ?? 'none' }`; useEffect( () => { diff --git a/packages/components/src/tooltip/test/index.js b/packages/components/src/tooltip/test/index.js index 9e827c6172b15..21892dea6a0da 100644 --- a/packages/components/src/tooltip/test/index.js +++ b/packages/components/src/tooltip/test/index.js @@ -212,7 +212,7 @@ describe( 'Tooltip', () => { // Note: this is testing for implementation details, // but couldn't find a better way. const buttonRect = button.getBoundingClientRect(); - // eslint-disable-next-line testing-library/no-container + // eslint-disable-next-line testing-library/no-container, testing-library/no-node-access const eventCatcher = container.querySelector( '.event-catcher' ); const eventCatcherRect = eventCatcher.getBoundingClientRect(); expect( buttonRect ).toEqual( eventCatcherRect ); @@ -252,7 +252,7 @@ describe( 'Tooltip', () => { ); - // eslint-disable-next-line testing-library/no-container + // eslint-disable-next-line testing-library/no-container, testing-library/no-node-access const eventCatcher = container.querySelector( '.event-catcher' ); await user.click( eventCatcher ); expect( onClickMock ).not.toHaveBeenCalled(); diff --git a/packages/components/src/unit-control/test/index.tsx b/packages/components/src/unit-control/test/index.tsx index 32e565fec2386..1703b3ad34db4 100644 --- a/packages/components/src/unit-control/test/index.tsx +++ b/packages/components/src/unit-control/test/index.tsx @@ -115,7 +115,7 @@ describe( 'UnitControl', () => { withoutClassName.querySelector( '.components-unit-control' ) ).not.toHaveClass( 'hello' ); expect( - // eslint-disable-next-line testing-library/no-container + // eslint-disable-next-line testing-library/no-container, testing-library/no-node-access withClassName.querySelector( '.components-unit-control' ) ).toHaveClass( 'hello' ); } ); diff --git a/packages/compose/src/higher-order/with-safe-timeout/index.tsx b/packages/compose/src/higher-order/with-safe-timeout/index.tsx index 4056e64ba65e5..d15b8a72d41ea 100644 --- a/packages/compose/src/higher-order/with-safe-timeout/index.tsx +++ b/packages/compose/src/higher-order/with-safe-timeout/index.tsx @@ -59,7 +59,9 @@ const withSafeTimeout = createHigherOrderComponent( clearTimeout( id: number ) { clearTimeout( id ); - this.timeouts.filter( ( timeoutId ) => timeoutId !== id ); + this.timeouts = this.timeouts.filter( + ( timeoutId ) => timeoutId !== id + ); } render() { diff --git a/packages/core-data/src/actions.js b/packages/core-data/src/actions.js index 550264d6232ae..307e5cef0e229 100644 --- a/packages/core-data/src/actions.js +++ b/packages/core-data/src/actions.js @@ -2,7 +2,6 @@ * External dependencies */ import fastDeepEqual from 'fast-deep-equal/es6'; -import { find } from 'lodash'; import { v4 as uuid } from 'uuid'; /** @@ -256,7 +255,9 @@ export const deleteEntityRecord = ) => async ( { dispatch } ) => { const configs = await dispatch( getOrLoadEntitiesConfig( kind ) ); - const entityConfig = find( configs, { kind, name } ); + const entityConfig = configs.find( + ( config ) => config.kind === kind && config.name === name + ); let error; let deletedRecord = false; if ( ! entityConfig || entityConfig?.__experimentalNoFetch ) { @@ -451,7 +452,9 @@ export const saveEntityRecord = ) => async ( { select, resolveSelect, dispatch } ) => { const configs = await dispatch( getOrLoadEntitiesConfig( kind ) ); - const entityConfig = find( configs, { kind, name } ); + const entityConfig = configs.find( + ( config ) => config.kind === kind && config.name === name + ); if ( ! entityConfig || entityConfig?.__experimentalNoFetch ) { return; } @@ -723,7 +726,9 @@ export const saveEditedEntityRecord = return; } const configs = await dispatch( getOrLoadEntitiesConfig( kind ) ); - const entityConfig = find( configs, { kind, name } ); + const entityConfig = configs.find( + ( config ) => config.kind === kind && config.name === name + ); if ( ! entityConfig ) { return; } diff --git a/packages/core-data/src/entities.js b/packages/core-data/src/entities.js index 0b64c3362e895..2fc6f4d41d768 100644 --- a/packages/core-data/src/entities.js +++ b/packages/core-data/src/entities.js @@ -2,7 +2,7 @@ * External dependencies */ import { capitalCase, pascalCase } from 'change-case'; -import { map, find, get } from 'lodash'; +import { map, get } from 'lodash'; /** * WordPress dependencies @@ -288,7 +288,9 @@ export const getMethodName = ( prefix = 'get', usePlural = false ) => { - const entityConfig = find( rootEntitiesConfig, { kind, name } ); + const entityConfig = rootEntitiesConfig.find( + ( config ) => config.kind === kind && config.name === name + ); const kindPrefix = kind === 'root' ? '' : pascalCase( kind ); const nameSuffix = pascalCase( name ) + ( usePlural ? 's' : '' ); const suffix = @@ -313,7 +315,9 @@ export const getOrLoadEntitiesConfig = return configs; } - const loader = find( additionalEntityConfigLoaders, { kind } ); + const loader = additionalEntityConfigLoaders.find( + ( l ) => l.kind === kind + ); if ( ! loader ) { return []; } diff --git a/packages/core-data/src/selectors.ts b/packages/core-data/src/selectors.ts index 93306d2226e7a..9998d67728e74 100644 --- a/packages/core-data/src/selectors.ts +++ b/packages/core-data/src/selectors.ts @@ -2,7 +2,7 @@ * External dependencies */ import createSelector from 'rememo'; -import { set, map, find, get } from 'lodash'; +import { set, map, get } from 'lodash'; /** * WordPress dependencies @@ -218,7 +218,9 @@ export function getEntityConfig( kind: string, name: string ): any { - return find( state.entities.config, { kind, name } ); + return state.entities.config?.find( + ( config ) => config.kind === kind && config.name === name + ); } /** @@ -1129,7 +1131,10 @@ export function getAutosave< EntityRecord extends ET.EntityRecord< any > >( } const autosaves = state.autosaves[ postId ]; - return find( autosaves, { author: authorId } ) as EntityRecord | undefined; + + return autosaves?.find( + ( autosave: any ) => autosave.author === authorId + ) as EntityRecord | undefined; } /** diff --git a/packages/customize-widgets/src/components/keyboard-shortcuts/index.js b/packages/customize-widgets/src/components/keyboard-shortcuts/index.js index 56888f1cfe3ae..5c5f3ea2bca83 100644 --- a/packages/customize-widgets/src/components/keyboard-shortcuts/index.js +++ b/packages/customize-widgets/src/components/keyboard-shortcuts/index.js @@ -6,6 +6,7 @@ import { useShortcut, store as keyboardShortcutsStore, } from '@wordpress/keyboard-shortcuts'; +import { isAppleOS } from '@wordpress/keycodes'; import { useDispatch } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; @@ -52,6 +53,18 @@ function KeyboardShortcutsRegister() { modifier: 'primaryShift', character: 'z', }, + // Disable on Apple OS because it conflicts with the browser's + // history shortcut. It's a fine alias for both Windows and Linux. + // Since there's no conflict for Ctrl+Shift+Z on both Windows and + // Linux, we keep it as the default for consistency. + aliases: isAppleOS() + ? [] + : [ + { + modifier: 'primary', + character: 'y', + }, + ], } ); registerShortcut( { diff --git a/packages/customize-widgets/src/utils.js b/packages/customize-widgets/src/utils.js index f2f254b5ae6af..8cf2d5990423c 100644 --- a/packages/customize-widgets/src/utils.js +++ b/packages/customize-widgets/src/utils.js @@ -98,10 +98,14 @@ export function widgetToBlock( { id, idBase, number, instance } ) { const { encoded_serialized_instance: encoded, instance_hash_key: hash, - raw_instance: raw, + raw_instance: rawInstance, ...rest } = instance; + // It's unclear why `content` is sometimes `undefined`, but it shouldn't be. + const rawContent = rawInstance.content || ''; + const raw = { ...rawInstance, content: rawContent }; + if ( idBase === 'block' ) { const parsedBlocks = parse( raw.content, { __unstableSkipAutop: true, diff --git a/packages/data/src/plugins/persistence/test/index.js b/packages/data/src/plugins/persistence/test/index.js index 818f075640ae5..e73af963bbeca 100644 --- a/packages/data/src/plugins/persistence/test/index.js +++ b/packages/data/src/plugins/persistence/test/index.js @@ -26,9 +26,10 @@ describe( 'persistence', () => { } ); it( 'should not mutate options', () => { - const options = Object.freeze( { persist: true, reducer() {} } ); - - registry.registerStore( 'test', options ); + expect( () => { + const options = Object.freeze( { persist: true, reducer() {} } ); + registry.registerStore( 'test', options ); + } ).not.toThrowError( /object is not extensible/ ); } ); it( 'should load a persisted value as initialState', () => { diff --git a/packages/data/src/test/registry.js b/packages/data/src/test/registry.js index 8f73b3e442900..a40d7628d5dc2 100644 --- a/packages/data/src/test/registry.js +++ b/packages/data/src/test/registry.js @@ -1,3 +1,5 @@ +/* eslint jest/expect-expect: ["warn", { "assertFunctionNames": ["expect", "subscribeUntil"] }] */ + /** * Internal dependencies */ diff --git a/packages/dom/src/tabbable.js b/packages/dom/src/tabbable.js index 66ae1ee7d4fd4..25bc81f215458 100644 --- a/packages/dom/src/tabbable.js +++ b/packages/dom/src/tabbable.js @@ -159,18 +159,15 @@ export function find( context ) { * @return {Element|undefined} Preceding tabbable element. */ export function findPrevious( element ) { - const focusables = findFocusable( element.ownerDocument.body ); - const index = focusables.indexOf( element ); - - if ( index === -1 ) { - return undefined; - } - - // Remove all focusables after and including `element`. - focusables.length = index; - - const tabbable = filterTabbable( focusables ); - return tabbable[ tabbable.length - 1 ]; + return filterTabbable( findFocusable( element.ownerDocument.body ) ) + .reverse() + .find( ( focusable ) => { + return ( + // eslint-disable-next-line no-bitwise + element.compareDocumentPosition( focusable ) & + element.DOCUMENT_POSITION_PRECEDING + ); + } ); } /** @@ -182,11 +179,13 @@ export function findPrevious( element ) { * @return {Element|undefined} Next tabbable element. */ export function findNext( element ) { - const focusables = findFocusable( element.ownerDocument.body ); - const index = focusables.indexOf( element ); - - // Remove all focusables before and including `element`. - const remaining = focusables.slice( index + 1 ); - - return filterTabbable( remaining )[ 0 ]; + return filterTabbable( findFocusable( element.ownerDocument.body ) ).find( + ( focusable ) => { + return ( + // eslint-disable-next-line no-bitwise + element.compareDocumentPosition( focusable ) & + element.DOCUMENT_POSITION_FOLLOWING + ); + } + ); } diff --git a/packages/e2e-tests/assets/small-post-with-containers.html b/packages/e2e-tests/assets/small-post-with-containers.html new file mode 100644 index 0000000000000..f5d32011602dc --- /dev/null +++ b/packages/e2e-tests/assets/small-post-with-containers.html @@ -0,0 +1,77 @@ + +
+
+

Heading

+ + + +

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

+ + + +
    +
  • one
  • + + + +
  • two
  • + + + +
  • three
  • +
+
+ + + +
+

Heading

+ + + +

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

+ + + +
    +
  • one
  • + + + +
  • two
  • + + + +
  • three
  • +
+
+ + + +
+

Heading

+ + + +

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

+ + + +
    +
  • one
  • + + + +
  • two
  • + + + +
  • three
  • +
+
+
+ + + +

+ \ No newline at end of file diff --git a/packages/e2e-tests/config/performance-reporter.js b/packages/e2e-tests/config/performance-reporter.js index 549855bf29713..e9928008dd6e3 100644 --- a/packages/e2e-tests/config/performance-reporter.js +++ b/packages/e2e-tests/config/performance-reporter.js @@ -36,6 +36,7 @@ class PerformanceReporter { firstContentfulPaint, firstBlock, type, + typeContainer, focus, listViewOpen, inserterOpen, @@ -68,7 +69,7 @@ Average time to first block: ${ success( if ( type && type.length ) { // eslint-disable-next-line no-console console.log( ` -${ title( 'Typing Performance:' ) } +${ title( 'Typing:' ) } Average time to type character: ${ success( round( average( type ) ) + 'ms' ) } Slowest time to type character: ${ success( round( Math.max( ...type ) ) + 'ms' @@ -78,10 +79,25 @@ Fastest time to type character: ${ success( ) }` ); } + if ( typeContainer && typeContainer.length ) { + // eslint-disable-next-line no-console + console.log( ` +${ title( 'Typing within a container:' ) } +Average time to type within a container: ${ success( + round( average( typeContainer ) ) + 'ms' + ) } +Slowest time to type within a container: ${ success( + round( Math.max( ...typeContainer ) ) + 'ms' + ) } +Fastest time to type within a container: ${ success( + round( Math.min( ...typeContainer ) ) + 'ms' + ) }` ); + } + if ( focus && focus.length ) { // eslint-disable-next-line no-console console.log( ` -${ title( 'Block Selection Performance:' ) } +${ title( 'Block Selection:' ) } Average time to select a block: ${ success( round( average( focus ) ) + 'ms' ) } Slowest time to select a block: ${ success( round( Math.max( ...focus ) ) + 'ms' @@ -94,7 +110,7 @@ Fastest time to select a block: ${ success( if ( listViewOpen && listViewOpen.length ) { // eslint-disable-next-line no-console console.log( ` -${ title( 'Opening List View Performance:' ) } +${ title( 'Opening List View:' ) } Average time to open list view: ${ success( round( average( listViewOpen ) ) + 'ms' ) } @@ -109,7 +125,7 @@ Fastest time to open list view: ${ success( if ( inserterOpen && inserterOpen.length ) { // eslint-disable-next-line no-console console.log( ` -${ title( 'Opening Global Inserter Performance:' ) } +${ title( 'Opening Global Inserter:' ) } Average time to open global inserter: ${ success( round( average( inserterOpen ) ) + 'ms' ) } @@ -124,7 +140,7 @@ Fastest time to open global inserter: ${ success( if ( inserterSearch && inserterSearch.length ) { // eslint-disable-next-line no-console console.log( ` -${ title( 'Inserter Search Performance:' ) } +${ title( 'Inserter Search:' ) } Average time to type the inserter search input: ${ success( round( average( inserterSearch ) ) + 'ms' ) } @@ -139,7 +155,7 @@ Fastest time to type the inserter search input: ${ success( if ( inserterHover && inserterHover.length ) { // eslint-disable-next-line no-console console.log( ` -${ title( 'Inserter Block Item Hover Performance:' ) } +${ title( 'Inserter Block Item Hover:' ) } Average time to move mouse between two block item in the inserter: ${ success( round( average( inserterHover ) ) + 'ms' ) } diff --git a/packages/e2e-tests/specs/editor/blocks/__snapshots__/quote.test.js.snap b/packages/e2e-tests/specs/editor/blocks/__snapshots__/quote.test.js.snap deleted file mode 100644 index 9a3924de6d1da..0000000000000 --- a/packages/e2e-tests/specs/editor/blocks/__snapshots__/quote.test.js.snap +++ /dev/null @@ -1,123 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Quote can be converted to a pullquote 1`] = ` -" -

one
two

cite
-" -`; - -exports[`Quote can be converted to paragraphs and renders a paragraph for the cite, if it exists 1`] = ` -" -

one

- - - -

two

- - - -

cite

-" -`; - -exports[`Quote can be converted to paragraphs and renders a void paragraph if both the cite and quote are void 1`] = `""`; - -exports[`Quote can be converted to paragraphs and renders one paragraph block per

within quote 1`] = ` -" -

one

- - - -

two

-" -`; - -exports[`Quote can be converted to paragraphs and renders only one paragraph for the cite, if the quote is void 1`] = ` -" -

- - - -

cite

-" -`; - -exports[`Quote can be created by converting a heading 1`] = ` -" -
-

test

-
-" -`; - -exports[`Quote can be created by converting a paragraph 1`] = ` -" -
-

test

-
-" -`; - -exports[`Quote can be created by converting multiple paragraphs 1`] = ` -" -
-

one

- - - -

two

-
-" -`; - -exports[`Quote can be created by typing "/quote" 1`] = ` -" -
-

I’m a quote

-
-" -`; - -exports[`Quote can be created by typing > in front of text of a paragraph block 1`] = ` -" -
-

test

-
-" -`; - -exports[`Quote can be created by using > at the start of a paragraph block 1`] = ` -" -
-

A quote

- - - -

Another paragraph

-
-" -`; - -exports[`Quote can be split at the end 1`] = ` -" -
-

1

-
- - - -

-" -`; - -exports[`Quote can be split at the end 2`] = ` -" -
-

1

- - - -

2

-
-" -`; diff --git a/packages/e2e-tests/specs/editor/blocks/quote.test.js b/packages/e2e-tests/specs/editor/blocks/quote.test.js deleted file mode 100644 index 9b0a4bac546dd..0000000000000 --- a/packages/e2e-tests/specs/editor/blocks/quote.test.js +++ /dev/null @@ -1,199 +0,0 @@ -/** - * WordPress dependencies - */ -import { - clickBlockAppender, - getEditedPostContent, - createNewPost, - pressKeyTimes, - transformBlockTo, - insertBlock, -} from '@wordpress/e2e-test-utils'; - -describe( 'Quote', () => { - beforeEach( async () => { - await createNewPost(); - } ); - - it( 'can be created by using > at the start of a paragraph block', async () => { - // Create a block with some text that will trigger a list creation. - await clickBlockAppender(); - await page.keyboard.type( '> A quote' ); - - // Create a second list item. - await page.keyboard.press( 'Enter' ); - await page.keyboard.type( 'Another paragraph' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'can be created by typing > in front of text of a paragraph block', async () => { - // Create a list with the slash block shortcut. - await clickBlockAppender(); - await page.keyboard.type( 'test' ); - await pressKeyTimes( 'ArrowLeft', 4 ); - await page.keyboard.type( '> ' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'can be created by typing "/quote"', async () => { - // Create a list with the slash block shortcut. - await clickBlockAppender(); - await page.keyboard.type( '/quote' ); - await page.waitForXPath( - `//*[contains(@class, "components-autocomplete__result") and contains(@class, "is-selected") and contains(text(), 'Quote')]` - ); - await page.keyboard.press( 'Enter' ); - await page.keyboard.type( 'I’m a quote' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'can be created by converting a paragraph', async () => { - await clickBlockAppender(); - await page.keyboard.type( 'test' ); - await transformBlockTo( 'Quote' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'can be created by converting multiple paragraphs', async () => { - await clickBlockAppender(); - await page.keyboard.type( 'one' ); - await page.keyboard.press( 'Enter' ); - await page.keyboard.type( 'two' ); - await page.keyboard.down( 'Shift' ); - await page.click( '[data-type="core/paragraph"]' ); - await page.keyboard.up( 'Shift' ); - await transformBlockTo( 'Quote' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - describe( 'can be converted to paragraphs', () => { - it( 'and renders one paragraph block per

within quote', async () => { - await insertBlock( 'Quote' ); - await page.keyboard.type( 'one' ); - await page.keyboard.press( 'Enter' ); - await page.keyboard.type( 'two' ); - // Navigate to the citation to select the block. - await page.keyboard.press( 'ArrowRight' ); - await transformBlockTo( 'Unwrap' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'and renders a paragraph for the cite, if it exists', async () => { - await insertBlock( 'Quote' ); - await page.keyboard.type( 'one' ); - await page.keyboard.press( 'Enter' ); - await page.keyboard.type( 'two' ); - await page.keyboard.press( 'ArrowRight' ); - await page.keyboard.type( 'cite' ); - await transformBlockTo( 'Unwrap' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'and renders only one paragraph for the cite, if the quote is void', async () => { - await insertBlock( 'Quote' ); - await page.keyboard.press( 'ArrowRight' ); - await page.keyboard.type( 'cite' ); - await transformBlockTo( 'Unwrap' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'and renders a void paragraph if both the cite and quote are void', async () => { - await insertBlock( 'Quote' ); - await page.keyboard.press( 'ArrowRight' ); // Select the quote - await transformBlockTo( 'Unwrap' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - } ); - - it( 'can be created by converting a heading', async () => { - await insertBlock( 'Heading' ); - await page.keyboard.type( 'test' ); - await transformBlockTo( 'Quote' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'can be converted to a pullquote', async () => { - await insertBlock( 'Quote' ); - await page.keyboard.type( 'one' ); - await page.keyboard.press( 'Enter' ); - await page.keyboard.type( 'two' ); - await page.keyboard.press( 'ArrowRight' ); - await page.keyboard.type( 'cite' ); - await transformBlockTo( 'Pullquote' ); - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'can be split at the end', async () => { - await insertBlock( 'Quote' ); - await page.keyboard.type( '1' ); - await page.keyboard.press( 'Enter' ); - await page.keyboard.press( 'Enter' ); - - // Expect empty paragraph outside quote block. - expect( await getEditedPostContent() ).toMatchSnapshot(); - - await page.keyboard.press( 'Backspace' ); - // Allow time for selection to update. - await page.evaluate( () => new Promise( window.requestIdleCallback ) ); - await page.keyboard.type( '2' ); - - // Expect the paragraph to be merged into the quote block. - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'can be unwrapped on Backspace', async () => { - await insertBlock( 'Quote' ); - - expect( await getEditedPostContent() ).toMatchInlineSnapshot( ` - " -

-

-
- " - ` ); - - await page.keyboard.press( 'Backspace' ); - - expect( await getEditedPostContent() ).toMatchInlineSnapshot( `""` ); - } ); - - it( 'can be unwrapped with content on Backspace', async () => { - await insertBlock( 'Quote' ); - await page.keyboard.type( '1' ); - await page.keyboard.press( 'ArrowRight' ); - await page.keyboard.type( '2' ); - - expect( await getEditedPostContent() ).toMatchInlineSnapshot( ` - " -
-

1

- 2
- " - ` ); - - await page.keyboard.press( 'ArrowLeft' ); - await page.keyboard.press( 'ArrowUp' ); - await page.keyboard.press( 'ArrowUp' ); - await page.keyboard.press( 'Backspace' ); - - expect( await getEditedPostContent() ).toMatchInlineSnapshot( ` - " -

1

- - - -
2
- " - ` ); - } ); -} ); diff --git a/packages/e2e-tests/specs/editor/various/multi-block-selection.test.js b/packages/e2e-tests/specs/editor/various/multi-block-selection.test.js index 3d0db13db8ad0..6afe91c6e9257 100644 --- a/packages/e2e-tests/specs/editor/various/multi-block-selection.test.js +++ b/packages/e2e-tests/specs/editor/various/multi-block-selection.test.js @@ -70,7 +70,7 @@ async function testNativeSelection() { return; } - if ( ! selection.rangeCount === 1 ) { + if ( selection.rangeCount !== 1 ) { throw 'expected one range'; } diff --git a/packages/e2e-tests/specs/performance/post-editor.test.js b/packages/e2e-tests/specs/performance/post-editor.test.js index 933b4973fb055..63fa92514728e 100644 --- a/packages/e2e-tests/specs/performance/post-editor.test.js +++ b/packages/e2e-tests/specs/performance/post-editor.test.js @@ -32,6 +32,23 @@ import { jest.setTimeout( 1000000 ); +async function loadHtmlIntoTheBlockEditor( html ) { + await page.evaluate( ( _html ) => { + const { parse } = window.wp.blocks; + const { dispatch } = window.wp.data; + const blocks = parse( _html ); + + blocks.forEach( ( block ) => { + if ( block.name === 'core/image' ) { + delete block.attributes.id; + delete block.attributes.url; + } + } ); + + dispatch( 'core/block-editor' ).resetBlocks( blocks ); + }, html ); +} + describe( 'Post Editor Performance', () => { const results = { serverResponse: [], @@ -41,6 +58,7 @@ describe( 'Post Editor Performance', () => { firstContentfulPaint: [], firstBlock: [], type: [], + typeContainer: [], focus: [], listViewOpen: [], inserterOpen: [], @@ -51,25 +69,10 @@ describe( 'Post Editor Performance', () => { let traceResults; beforeAll( async () => { - const html = readFile( - join( __dirname, '../../assets/large-post.html' ) - ); - await createNewPost(); - await page.evaluate( ( _html ) => { - const { parse } = window.wp.blocks; - const { dispatch } = window.wp.data; - const blocks = parse( _html ); - - blocks.forEach( ( block ) => { - if ( block.name === 'core/image' ) { - delete block.attributes.id; - delete block.attributes.url; - } - } ); - - dispatch( 'core/block-editor' ).resetBlocks( blocks ); - }, html ); + await loadHtmlIntoTheBlockEditor( + readFile( join( __dirname, '../../assets/large-post.html' ) ) + ); await saveDraft(); } ); @@ -151,6 +154,58 @@ describe( 'Post Editor Performance', () => { } } ); + it( 'Typing within containers', async () => { + // Measuring block selection performance. + await createNewPost(); + await loadHtmlIntoTheBlockEditor( + readFile( + join( + __dirname, + '../../assets/small-post-with-containers.html' + ) + ) + ); + // Select the block where we type in + await page.waitForSelector( 'p[aria-label="Paragraph block"]' ); + await page.click( 'p[aria-label="Paragraph block"]' ); + // Ignore firsted typed character because it's different + // It probably deserves a dedicated metric. + // (isTyping triggers so it's slower) + await page.keyboard.type( 'x' ); + + let i = 10; + await page.tracing.start( { + path: traceFile, + screenshots: false, + categories: [ 'devtools.timeline' ], + } ); + + while ( i-- ) { + // Wait for the browser to be idle before starting the monitoring. + // eslint-disable-next-line no-restricted-syntax + await page.waitForTimeout( 500 ); + await page.keyboard.type( 'x' ); + } + // eslint-disable-next-line no-restricted-syntax + await page.waitForTimeout( 500 ); + await page.tracing.stop(); + traceResults = JSON.parse( readFile( traceFile ) ); + const [ keyDownEvents, keyPressEvents, keyUpEvents ] = + getTypingEventDurations( traceResults ); + if ( + keyDownEvents.length === keyPressEvents.length && + keyPressEvents.length === keyUpEvents.length + ) { + // The first character typed triggers a longer time (isTyping change) + // It can impact the stability of the metric, so we exclude it. + for ( let j = 1; j < keyDownEvents.length; j++ ) { + results.typeContainer.push( + keyDownEvents[ j ] + keyPressEvents[ j ] + keyUpEvents[ j ] + ); + } + } + } ); + it( 'Selecting blocks', async () => { // Measuring block selection performance. await createNewPost(); diff --git a/packages/e2e-tests/specs/performance/site-editor.test.js b/packages/e2e-tests/specs/performance/site-editor.test.js index eaa3323b7b16c..e3a2ef86c6932 100644 --- a/packages/e2e-tests/specs/performance/site-editor.test.js +++ b/packages/e2e-tests/specs/performance/site-editor.test.js @@ -51,6 +51,7 @@ describe( 'Site Editor Performance', () => { firstContentfulPaint: [], firstBlock: [], type: [], + typeContainer: [], focus: [], inserterOpen: [], inserterHover: [], diff --git a/packages/edit-navigation/src/components/layout/shortcuts.js b/packages/edit-navigation/src/components/layout/shortcuts.js index 1ced3ba2183fe..8036d0534261e 100644 --- a/packages/edit-navigation/src/components/layout/shortcuts.js +++ b/packages/edit-navigation/src/components/layout/shortcuts.js @@ -7,6 +7,7 @@ import { useShortcut, store as keyboardShortcutsStore, } from '@wordpress/keyboard-shortcuts'; +import { isAppleOS } from '@wordpress/keycodes'; import { __ } from '@wordpress/i18n'; import { store as coreStore } from '@wordpress/core-data'; @@ -59,6 +60,18 @@ function RegisterNavigationEditorShortcuts() { modifier: 'primaryShift', character: 'z', }, + // Disable on Apple OS because it conflicts with the browser's + // history shortcut. It's a fine alias for both Windows and Linux. + // Since there's no conflict for Ctrl+Shift+Z on both Windows and + // Linux, we keep it as the default for consistency. + aliases: isAppleOS() + ? [] + : [ + { + modifier: 'primary', + character: 'y', + }, + ], } ); }, [ registerShortcut ] ); diff --git a/packages/edit-post/package.json b/packages/edit-post/package.json index 3414e958344d2..7967b0d63b1d6 100644 --- a/packages/edit-post/package.json +++ b/packages/edit-post/package.json @@ -52,6 +52,7 @@ "@wordpress/url": "file:../url", "@wordpress/viewport": "file:../viewport", "@wordpress/warning": "file:../warning", + "@wordpress/widgets": "file:../widgets", "classnames": "^2.3.1", "lodash": "^4.17.21", "memize": "^1.1.0", diff --git a/packages/edit-post/src/index.js b/packages/edit-post/src/index.js index 5b7c4e01ead7b..2ee4dcdab1704 100644 --- a/packages/edit-post/src/index.js +++ b/packages/edit-post/src/index.js @@ -10,6 +10,7 @@ import { render, unmountComponentAtNode } from '@wordpress/element'; import { dispatch, select } from '@wordpress/data'; import { addFilter } from '@wordpress/hooks'; import { store as preferencesStore } from '@wordpress/preferences'; +import { registerLegacyWidgetBlock } from '@wordpress/widgets'; /** * Internal dependencies @@ -115,6 +116,7 @@ export function initializeEditor( } registerCoreBlocks(); + registerLegacyWidgetBlock( { inserter: false } ); if ( process.env.IS_GUTENBERG_PLUGIN ) { __experimentalRegisterExperimentalCoreBlocks( { enableFSEBlocks: settings.__unstableEnableFullSiteEditingBlocks, diff --git a/packages/edit-site/CHANGELOG.md b/packages/edit-site/CHANGELOG.md index 83dcb848d66b3..357668d600d11 100644 --- a/packages/edit-site/CHANGELOG.md +++ b/packages/edit-site/CHANGELOG.md @@ -4,7 +4,11 @@ ### Breaking Changes -- Updated dependencies to require React 18 ([45235](https://github.com/WordPress/gutenberg/pull/45235)) +- Updated dependencies to require React 18 ([45235](https://github.com/WordPress/gutenberg/pull/45235)). + +### Enhancements + +- Fluid typography: add configurable fluid typography settings for minimum font size to theme.json ([#42489](https://github.com/WordPress/gutenberg/pull/42489)). ## 4.19.0 (2022-11-16) diff --git a/packages/edit-site/package.json b/packages/edit-site/package.json index 6f42f00b2c625..a89f52e5dddda 100644 --- a/packages/edit-site/package.json +++ b/packages/edit-site/package.json @@ -54,6 +54,7 @@ "@wordpress/style-engine": "file:../style-engine", "@wordpress/url": "file:../url", "@wordpress/viewport": "file:../viewport", + "@wordpress/widgets": "file:../widgets", "classnames": "^2.3.1", "colord": "^2.9.2", "downloadjs": "^1.4.7", diff --git a/packages/edit-site/src/components/add-new-template/index.js b/packages/edit-site/src/components/add-new-template/index.js index 89abbc0aa4614..c3ba06f065bb9 100644 --- a/packages/edit-site/src/components/add-new-template/index.js +++ b/packages/edit-site/src/components/add-new-template/index.js @@ -10,7 +10,10 @@ import { store as coreStore } from '@wordpress/core-data'; import NewTemplate from './new-template'; import NewTemplatePart from './new-template-part'; -export default function AddNewTemplate( { templateType = 'wp_template' } ) { +export default function AddNewTemplate( { + templateType = 'wp_template', + ...props +} ) { const postType = useSelect( ( select ) => select( coreStore ).getPostType( templateType ), [ templateType ] @@ -21,9 +24,9 @@ export default function AddNewTemplate( { templateType = 'wp_template' } ) { } if ( templateType === 'wp_template' ) { - return ; + return ; } else if ( templateType === 'wp_template_part' ) { - return ; + return ; } return null; diff --git a/packages/edit-site/src/components/add-new-template/new-template-part.js b/packages/edit-site/src/components/add-new-template/new-template-part.js index 783545bac826b..578cfa515bd2c 100644 --- a/packages/edit-site/src/components/add-new-template/new-template-part.js +++ b/packages/edit-site/src/components/add-new-template/new-template-part.js @@ -12,6 +12,7 @@ import { Button } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { store as noticesStore } from '@wordpress/notices'; import { store as coreStore } from '@wordpress/core-data'; +import { plus } from '@wordpress/icons'; /** * Internal dependencies @@ -20,7 +21,11 @@ import { useHistory } from '../routes'; import { store as editSiteStore } from '../../store'; import CreateTemplatePartModal from '../create-template-part-modal'; -export default function NewTemplatePart( { postType } ) { +export default function NewTemplatePart( { + postType, + showIcon = true, + toggleProps, +} ) { const history = useHistory(); const [ isModalOpen, setIsModalOpen ] = useState( false ); const { createErrorNotice } = useDispatch( noticesStore ); @@ -83,12 +88,14 @@ export default function NewTemplatePart( { postType } ) { return ( <> { isModalOpen && ( { () => ( <> diff --git a/packages/edit-site/src/components/add-new-template/style.scss b/packages/edit-site/src/components/add-new-template/style.scss index 51fb9af347c2c..f247bfa214cfb 100644 --- a/packages/edit-site/src/components/add-new-template/style.scss +++ b/packages/edit-site/src/components/add-new-template/style.scss @@ -1,8 +1,4 @@ .edit-site-new-template-dropdown { - .components-dropdown-menu__toggle { - padding: 6px 12px; - } - .edit-site-new-template-dropdown__popover { @include break-small() { min-width: 300px; diff --git a/packages/edit-site/src/components/editor/index.js b/packages/edit-site/src/components/editor/index.js index 2bb06063bc75a..fe733f49a1959 100644 --- a/packages/edit-site/src/components/editor/index.js +++ b/packages/edit-site/src/components/editor/index.js @@ -22,7 +22,6 @@ import { EntitiesSavedStates, } from '@wordpress/editor'; import { __ } from '@wordpress/i18n'; -import { store as keyboardShortcutsStore } from '@wordpress/keyboard-shortcuts'; /** * Internal dependencies @@ -31,7 +30,6 @@ import { SidebarComplementaryAreaFills } from '../sidebar-edit-mode'; import BlockEditor from '../block-editor'; import CodeEditor from '../code-editor'; import KeyboardShortcuts from '../keyboard-shortcuts'; -import useInitEditedEntityFromURL from '../sync-state-with-url/use-init-edited-entity-from-url'; import InserterSidebar from '../secondary-sidebar/inserter-sidebar'; import ListViewSidebar from '../secondary-sidebar/list-view-sidebar'; import WelcomeGuide from '../welcome-guide'; @@ -52,9 +50,6 @@ const interfaceLabels = { }; export default function Editor() { - // This ensures the edited entity id and type are initialized properly. - useInitEditedEntityFromURL(); - const { editedPostId, editedPostType, @@ -68,8 +63,6 @@ export default function Editor() { isInserterOpen, isListViewOpen, isSaveViewOpen, - previousShortcut, - nextShortcut, showIconLabels, } = useSelect( ( select ) => { const { @@ -84,9 +77,6 @@ export default function Editor() { } = select( editSiteStore ); const { hasFinishedResolution, getEntityRecord } = select( coreStore ); const { __unstableGetEditorMode } = select( blockEditorStore ); - const { getAllShortcutKeyCombinations } = select( - keyboardShortcutsStore - ); const { getActiveComplementaryArea } = select( interfaceStore ); const postType = getEditedPostType(); const postId = getEditedPostId(); @@ -116,12 +106,6 @@ export default function Editor() { isRightSidebarOpen: getActiveComplementaryArea( editSiteStore.name ), - previousShortcut: getAllShortcutKeyCombinations( - 'core/edit-site/previous-region' - ), - nextShortcut: getAllShortcutKeyCombinations( - 'core/edit-site/next-region' - ), showIconLabels: select( preferencesStore ).get( 'core/edit-site', 'showIconLabels' @@ -172,7 +156,6 @@ export default function Editor() { <> { isEditMode && } - + ) } - shortcuts={ { - previous: previousShortcut, - next: nextShortcut, - } } labels={ { ...interfaceLabels, secondarySidebar: secondarySidebarLabel, diff --git a/packages/edit-site/src/components/global-styles/custom-css.js b/packages/edit-site/src/components/global-styles/custom-css.js new file mode 100644 index 0000000000000..6c7d3de1cf06e --- /dev/null +++ b/packages/edit-site/src/components/global-styles/custom-css.js @@ -0,0 +1,73 @@ +/** + * WordPress dependencies + */ +import { TextareaControl, Panel, PanelBody } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import { useStyle } from './hooks'; + +function CustomCSSControl() { + const [ customCSS, setCustomCSS ] = useStyle( 'css' ); + const [ themeCSS ] = useStyle( 'css', null, 'base' ); + const ignoreThemeCustomCSS = '/* IgnoreThemeCustomCSS */'; + + // If there is custom css from theme.json show it in the edit box + // so the user can selectively overwrite it, rather than have the user CSS + // completely overwrite the theme CSS by default. + const themeCustomCSS = + ! customCSS && themeCSS + ? `/* ${ __( + 'Theme Custom CSS start' + ) } */\n${ themeCSS }\n/* ${ __( 'Theme Custom CSS end' ) } */` + : undefined; + + function handleOnChange( value ) { + // If there is theme custom CSS, but the user clears the input box then save the + // ignoreThemeCustomCSS string so that the theme custom CSS is not re-applied. + if ( themeCSS && value === '' ) { + setCustomCSS( ignoreThemeCustomCSS ); + return; + } + setCustomCSS( value ); + } + + const originalThemeCustomCSS = + themeCSS && customCSS && themeCustomCSS !== customCSS + ? themeCSS + : undefined; + + return ( + <> + handleOnChange( value ) } + rows={ 15 } + className="edit-site-global-styles__custom-css-input" + spellCheck={ false } + help={ __( + "Enter your custom CSS in the textarea and preview in the editor. Changes won't take effect until you've saved the template." + ) } + /> + { originalThemeCustomCSS && ( + + +
+							{ originalThemeCustomCSS }
+						
+
+
+ ) } + + ); +} + +export default CustomCSSControl; diff --git a/packages/edit-site/src/components/global-styles/global-styles-provider.js b/packages/edit-site/src/components/global-styles/global-styles-provider.js index 0ae154c11b91a..0813dc63b2cb8 100644 --- a/packages/edit-site/src/components/global-styles/global-styles-provider.js +++ b/packages/edit-site/src/components/global-styles/global-styles-provider.js @@ -94,7 +94,7 @@ function useGlobalStylesUserConfig() { }, [ settings, styles ] ); const setConfig = useCallback( - ( callback ) => { + ( callback, options = {} ) => { const record = getEditedEntityRecord( 'root', 'globalStyles', @@ -105,10 +105,16 @@ function useGlobalStylesUserConfig() { settings: record?.settings ?? {}, }; const updatedConfig = callback( currentConfig ); - editEntityRecord( 'root', 'globalStyles', globalStylesId, { - styles: cleanEmptyObject( updatedConfig.styles ) || {}, - settings: cleanEmptyObject( updatedConfig.settings ) || {}, - } ); + editEntityRecord( + 'root', + 'globalStyles', + globalStylesId, + { + styles: cleanEmptyObject( updatedConfig.styles ) || {}, + settings: cleanEmptyObject( updatedConfig.settings ) || {}, + }, + options + ); }, [ globalStylesId ] ); diff --git a/packages/edit-site/src/components/global-styles/hooks.js b/packages/edit-site/src/components/global-styles/hooks.js index 86e8aa2d2a641..c767f1a488cbf 100644 --- a/packages/edit-site/src/components/global-styles/hooks.js +++ b/packages/edit-site/src/components/global-styles/hooks.js @@ -138,7 +138,11 @@ export function useStyle( path, blockName, source = 'all' ) { result = getValueFromVariable( mergedConfig, blockName, - get( userConfig, finalPath ) ?? get( baseConfig, finalPath ) + // The stlyes.css path is allowed to be empty, so don't revert to base if undefined. + finalPath === 'styles.css' + ? get( userConfig, finalPath ) + : get( userConfig, finalPath ) ?? + get( baseConfig, finalPath ) ); break; case 'user': diff --git a/packages/edit-site/src/components/global-styles/screen-css.js b/packages/edit-site/src/components/global-styles/screen-css.js new file mode 100644 index 0000000000000..74480a0677bd1 --- /dev/null +++ b/packages/edit-site/src/components/global-styles/screen-css.js @@ -0,0 +1,33 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { __experimentalVStack as VStack } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import ScreenHeader from './header'; +import Subtitle from './subtitle'; +import CustomCSSControl from './custom-css'; + +function ScreenCSS() { + return ( + <> + +
+ + { __( 'ADDITIONAL CSS' ) } + + +
+ + ); +} + +export default ScreenCSS; diff --git a/packages/edit-site/src/components/global-styles/screen-root.js b/packages/edit-site/src/components/global-styles/screen-root.js index 0674b56319c70..ecb68d27b7f4d 100644 --- a/packages/edit-site/src/components/global-styles/screen-root.js +++ b/packages/edit-site/src/components/global-styles/screen-root.js @@ -75,7 +75,8 @@ function ScreenRoot() { paddingTop={ 2 } /* * 13px matches the text inset of the NavigationButton (12px padding, plus the width of the button's border). - * This is an ad hoc override for this particular instance only and should be reconsidered before making into a pattern. + * This is an ad hoc override for this instance and the Addtional CSS option below. Other options for matching the + * the nav button inset should be looked at before reusing further. */ paddingX="13px" marginBottom={ 4 } @@ -98,6 +99,34 @@ function ScreenRoot() { + + + + + + { __( + 'Add your own CSS to customize the appearance and layout of your site.' + ) } + + + + + { __( 'Custom' ) } + + + + + ); } diff --git a/packages/edit-site/src/components/global-styles/style.scss b/packages/edit-site/src/components/global-styles/style.scss index 16c9b9c418dca..5616a068b594c 100644 --- a/packages/edit-site/src/components/global-styles/style.scss +++ b/packages/edit-site/src/components/global-styles/style.scss @@ -31,7 +31,8 @@ $block-preview-height: 150px; } .edit-site-global-styles-screen-heading-color, -.edit-site-global-styles-screen-typography { +.edit-site-global-styles-screen-typography, +.edit-site-global-styles-screen-css { margin: $grid-unit-20; } @@ -133,3 +134,15 @@ $block-preview-height: 150px; border: $gray-200 $border-width solid; border-radius: $radius-block-ui; } + +.edit-site-global-styles__custom-css-input textarea { + font-family: $editor_html_font; +} + +.edit-site-global-styles__custom-css-theme-css { + width: 100%; + line-break: anywhere; + white-space: break-spaces; + max-height: 200px; + overflow-y: scroll; +} diff --git a/packages/edit-site/src/components/global-styles/test/typography-utils.js b/packages/edit-site/src/components/global-styles/test/typography-utils.js index 647b02cb4be1f..9d0213cb53e55 100644 --- a/packages/edit-site/src/components/global-styles/test/typography-utils.js +++ b/packages/edit-site/src/components/global-styles/test/typography-utils.js @@ -7,7 +7,8 @@ describe( 'typography utils', () => { describe( 'getTypographyFontSizeValue', () => { [ { - message: 'returns value when fluid typography is deactivated', + message: + 'should return value when fluid typography is not active', preset: { size: '28px', }, @@ -16,7 +17,7 @@ describe( 'typography utils', () => { }, { - message: 'returns value where font size is 0', + message: 'should return value where font size is 0', preset: { size: 0, }, @@ -25,7 +26,7 @@ describe( 'typography utils', () => { }, { - message: "returns value where font size is '0'", + message: "should return value where font size is '0'", preset: { size: '0', }, @@ -34,7 +35,7 @@ describe( 'typography utils', () => { }, { - message: 'returns value where `size` is `null`.', + message: 'should return value where `size` is `null`.', preset: { size: null, }, @@ -43,7 +44,7 @@ describe( 'typography utils', () => { }, { - message: 'returns value when fluid is `false`', + message: 'should return value when fluid is `false`', preset: { size: '28px', fluid: false, @@ -55,7 +56,7 @@ describe( 'typography utils', () => { }, { - message: 'returns already clamped value', + message: 'should return already clamped value', preset: { size: 'clamp(21px, 1.313rem + ((1vw - 7.68px) * 2.524), 42px)', fluid: false, @@ -68,7 +69,7 @@ describe( 'typography utils', () => { }, { - message: 'returns value with unsupported unit', + message: 'should return value with unsupported unit', preset: { size: '1000%', fluid: false, @@ -80,7 +81,7 @@ describe( 'typography utils', () => { }, { - message: 'returns clamp value with rem min and max units', + message: 'should return clamp value with rem min and max units', preset: { size: '1.75rem', }, @@ -92,7 +93,7 @@ describe( 'typography utils', () => { }, { - message: 'returns clamp value with eem min and max units', + message: 'should return clamp value with eem min and max units', preset: { size: '1.75em', }, @@ -104,7 +105,7 @@ describe( 'typography utils', () => { }, { - message: 'returns clamp value for floats', + message: 'should return clamp value for floats', preset: { size: '100.175px', }, @@ -116,7 +117,8 @@ describe( 'typography utils', () => { }, { - message: 'coerces integer to `px` and returns clamp value', + message: + 'should coerce integer to `px` and returns clamp value', preset: { size: 33, fluid: true, @@ -129,7 +131,7 @@ describe( 'typography utils', () => { }, { - message: 'coerces float to `px` and returns clamp value', + message: 'should coerce float to `px` and returns clamp value', preset: { size: 100.23, fluid: true, @@ -142,7 +144,8 @@ describe( 'typography utils', () => { }, { - message: 'returns clamp value when `fluid` is empty array', + message: + 'should return clamp value when `fluid` is empty array', preset: { size: '28px', fluid: [], @@ -155,7 +158,7 @@ describe( 'typography utils', () => { }, { - message: 'returns clamp value when `fluid` is `null`', + message: 'should return clamp value when `fluid` is `null`', preset: { size: '28px', fluid: null, @@ -169,7 +172,7 @@ describe( 'typography utils', () => { { message: - 'returns clamp value if min font size is greater than max', + 'should return clamp value if min font size is greater than max', preset: { size: '3rem', fluid: { @@ -185,7 +188,7 @@ describe( 'typography utils', () => { }, { - message: 'returns value with invalid min/max fluid units', + message: 'should return value with invalid min/max fluid units', preset: { size: '10em', fluid: { @@ -201,7 +204,7 @@ describe( 'typography utils', () => { { message: - 'returns value when size is < lower bounds and no fluid min/max set', + 'should return value when size is < lower bounds and no fluid min/max set', preset: { size: '3px', }, @@ -213,7 +216,7 @@ describe( 'typography utils', () => { { message: - 'returns value when size is equal to lower bounds and no fluid min/max set', + 'should return value when size is equal to lower bounds and no fluid min/max set', preset: { size: '14px', }, @@ -224,7 +227,8 @@ describe( 'typography utils', () => { }, { - message: 'returns clamp value with different min max units', + message: + 'should return clamp value with different min max units', preset: { size: '28px', fluid: { @@ -240,7 +244,8 @@ describe( 'typography utils', () => { }, { - message: 'returns clamp value where no fluid max size is set', + message: + 'should return clamp value where no fluid max size is set', preset: { size: '28px', fluid: { @@ -255,7 +260,8 @@ describe( 'typography utils', () => { }, { - message: 'returns clamp value where no fluid min size is set', + message: + 'should return clamp value where no fluid min size is set', preset: { size: '28px', fluid: { @@ -320,7 +326,7 @@ describe( 'typography utils', () => { { message: - 'returns clamp value when min and max font sizes are equal', + 'should return clamp value when min and max font sizes are equal', preset: { size: '4rem', fluid: { @@ -333,8 +339,51 @@ describe( 'typography utils', () => { }, expected: 'clamp(30px, 1.875rem + ((1vw - 7.68px) * 1), 30px)', }, + + { + message: + 'should use default min font size value where min font size unit in fluid config is not supported', + preset: { + size: '15px', + }, + typographySettings: { + fluid: { + minFontSize: '16%', + }, + }, + expected: + 'clamp(14px, 0.875rem + ((1vw - 7.68px) * 0.12), 15px)', + }, + + // Equivalent custom config PHP unit tests in `test_should_covert_font_sizes_to_fluid_values()`. + { + message: 'should return clamp value using custom fluid config', + preset: { + size: '17px', + }, + typographySettings: { + fluid: { + minFontSize: '16px', + }, + }, + expected: 'clamp(16px, 1rem + ((1vw - 7.68px) * 0.12), 17px)', + }, + + { + message: + 'should return value when font size <= custom min font size bound', + preset: { + size: '15px', + }, + typographySettings: { + fluid: { + minFontSize: '16px', + }, + }, + expected: '15px', + }, ].forEach( ( { message, preset, typographySettings, expected } ) => { - it( `should ${ message }`, () => { + it( `${ message }`, () => { expect( getTypographyFontSizeValue( preset, typographySettings ) ).toBe( expected ); diff --git a/packages/edit-site/src/components/global-styles/typography-utils.js b/packages/edit-site/src/components/global-styles/typography-utils.js index a792d1875005c..5720fdeb6ce91 100644 --- a/packages/edit-site/src/components/global-styles/typography-utils.js +++ b/packages/edit-site/src/components/global-styles/typography-utils.js @@ -23,13 +23,23 @@ import { getComputedFluidTypographyValue } from '@wordpress/block-editor'; * @property {boolean|FluidPreset|undefined} fluid A font size slug */ +/** + * @typedef {Object} TypographySettings + * @property {?string|?number} size A default font size. + * @property {?string} minViewPortWidth Minimum viewport size from which type will have fluidity. Optional if size is specified. + * @property {?string} maxViewPortWidth Maximum size up to which type will have fluidity. Optional if size is specified. + * @property {?number} scaleFactor A scale factor to determine how fast a font scales within boundaries. Optional. + * @property {?number} minFontSizeFactor How much to scale defaultFontSize by to derive minimumFontSize. Optional. + * @property {?string} minFontSize The smallest a calculated font size may be. Optional. + */ + /** * Returns a font-size value based on a given font-size preset. * Takes into account fluid typography parameters and attempts to return a css formula depending on available, valid values. * - * @param {Preset} preset - * @param {Object} typographySettings - * @param {boolean} typographySettings.fluid Whether fluid typography is enabled. + * @param {Preset} preset + * @param {Object} typographySettings + * @param {boolean|TypographySettings} typographySettings.fluid Whether fluid typography is enabled, and, optionally, fluid font size options. * * @return {string|*} A font-size value or the value of preset.size. */ @@ -44,7 +54,11 @@ export function getTypographyFontSizeValue( preset, typographySettings ) { return defaultSize; } - if ( true !== typographySettings?.fluid ) { + if ( + ! typographySettings?.fluid || + ( typeof typographySettings?.fluid === 'object' && + Object.keys( typographySettings.fluid ).length === 0 ) + ) { return defaultSize; } @@ -53,10 +67,16 @@ export function getTypographyFontSizeValue( preset, typographySettings ) { return defaultSize; } + const fluidTypographySettings = + typeof typographySettings?.fluid === 'object' + ? typographySettings?.fluid + : {}; + const fluidFontSizeValue = getComputedFluidTypographyValue( { minimumFontSize: preset?.fluid?.min, maximumFontSize: preset?.fluid?.max, fontSize: defaultSize, + minimumFontSizeLimit: fluidTypographySettings?.minFontSize, } ); if ( !! fluidFontSizeValue ) { diff --git a/packages/edit-site/src/components/global-styles/ui.js b/packages/edit-site/src/components/global-styles/ui.js index 532a211597a81..d7c9eed7d8474 100644 --- a/packages/edit-site/src/components/global-styles/ui.js +++ b/packages/edit-site/src/components/global-styles/ui.js @@ -27,6 +27,7 @@ import ScreenLayout from './screen-layout'; import ScreenStyleVariations from './screen-style-variations'; import ScreenBorder from './screen-border'; import StyleBook from '../style-book'; +import ScreenCSS from './screen-css'; function GlobalStylesNavigationScreen( { className, ...props } ) { return ( @@ -191,6 +192,9 @@ function GlobalStylesUI( { isStyleBookOpened, onCloseStyleBook } ) { { isStyleBookOpened && ( ) } + + + ); } diff --git a/packages/edit-site/src/components/global-styles/use-global-styles-output.js b/packages/edit-site/src/components/global-styles/use-global-styles-output.js index b32201fe2d2c2..d83693757b035 100644 --- a/packages/edit-site/src/components/global-styles/use-global-styles-output.js +++ b/packages/edit-site/src/components/global-styles/use-global-styles-output.js @@ -919,6 +919,11 @@ export function useGlobalStylesOutput() { css: globalStyles, isGlobalStyles: true, }, + // Load custom CSS in own stylesheet so that any invalid CSS entered in the input won't break all the global styles in the editor. + { + css: mergedConfig.styles.css ?? '', + isGlobalStyles: true, + }, ]; return [ stylesheets, mergedConfig.settings, filters ]; diff --git a/packages/edit-site/src/components/global-styles/utils.js b/packages/edit-site/src/components/global-styles/utils.js index 4c9d7bae19f7a..14f3b86829417 100644 --- a/packages/edit-site/src/components/global-styles/utils.js +++ b/packages/edit-site/src/components/global-styles/utils.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { get, find } from 'lodash'; +import { get } from 'lodash'; /** * Internal dependencies @@ -86,7 +86,7 @@ export const PRESET_METADATA = [ }, ]; -const STYLE_PATH_TO_CSS_VAR_INFIX = { +export const STYLE_PATH_TO_CSS_VAR_INFIX = { 'color.background': 'color', 'color.text': 'color', 'elements.link.color.text': 'color', @@ -100,6 +100,15 @@ const STYLE_PATH_TO_CSS_VAR_INFIX = { 'typography.fontFamily': 'font-family', }; +// A static list of block attributes that store global style preset slugs. +export const STYLE_PATH_TO_PRESET_BLOCK_ATTRIBUTE = { + 'color.background': 'backgroundColor', + 'color.text': 'textColor', + 'color.gradient': 'gradient', + 'typography.fontSize': 'fontSize', + 'typography.fontFamily': 'fontFamily', +}; + function findInPresetsBy( features, blockName, @@ -120,8 +129,7 @@ function findInPresetsBy( for ( const origin of origins ) { const presets = presetByOrigin[ origin ]; if ( presets ) { - const presetObject = find( - presets, + const presetObject = presets.find( ( preset ) => preset[ presetProperty ] === presetValueValue ); @@ -164,7 +172,9 @@ export function getPresetVariableFromValue( const cssVarInfix = STYLE_PATH_TO_CSS_VAR_INFIX[ variableStylePath ]; - const metadata = find( PRESET_METADATA, [ 'cssVarInfix', cssVarInfix ] ); + const metadata = PRESET_METADATA.find( + ( data ) => data.cssVarInfix === cssVarInfix + ); if ( ! metadata ) { // The property doesn't have preset data @@ -196,7 +206,9 @@ function getValueFromPresetVariable( variable, [ presetType, slug ] ) { - const metadata = find( PRESET_METADATA, [ 'cssVarInfix', presetType ] ); + const metadata = PRESET_METADATA.find( + ( data ) => data.cssVarInfix === presetType + ); if ( ! metadata ) { return variable; } diff --git a/packages/edit-site/src/components/keyboard-shortcuts/index.js b/packages/edit-site/src/components/keyboard-shortcuts/index.js index 96255dd928c60..8d4337dd097c5 100644 --- a/packages/edit-site/src/components/keyboard-shortcuts/index.js +++ b/packages/edit-site/src/components/keyboard-shortcuts/index.js @@ -6,6 +6,7 @@ import { useShortcut, store as keyboardShortcutsStore, } from '@wordpress/keyboard-shortcuts'; +import { isAppleOS } from '@wordpress/keycodes'; import { useDispatch, useSelect } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; import { store as coreStore } from '@wordpress/core-data'; @@ -119,6 +120,18 @@ function KeyboardShortcutsRegister() { modifier: 'primaryShift', character: 'z', }, + // Disable on Apple OS because it conflicts with the browser's + // history shortcut. It's a fine alias for both Windows and Linux. + // Since there's no conflict for Ctrl+Shift+Z on both Windows and + // Linux, we keep it as the default for consistency. + aliases: isAppleOS() + ? [] + : [ + { + modifier: 'primary', + character: 'y', + }, + ], } ); registerShortcut( { diff --git a/packages/edit-site/src/components/layout/index.js b/packages/edit-site/src/components/layout/index.js index ab3d90eabb83a..e3d2edc217eef 100644 --- a/packages/edit-site/src/components/layout/index.js +++ b/packages/edit-site/src/components/layout/index.js @@ -12,6 +12,7 @@ import { __experimentalHStack as HStack, __unstableMotion as motion, __unstableAnimatePresence as AnimatePresence, + __unstableUseNavigateRegions as useNavigateRegions, } from '@wordpress/components'; import { useReducedMotion, @@ -22,6 +23,7 @@ import { __ } from '@wordpress/i18n'; import { store as blockEditorStore } from '@wordpress/block-editor'; import { useState, useEffect } from '@wordpress/element'; import { NavigableRegion } from '@wordpress/interface'; +import { store as keyboardShortcutsStore } from '@wordpress/keyboard-shortcuts'; /** * Internal dependencies @@ -36,22 +38,39 @@ import getIsListPage from '../../utils/get-is-list-page'; import Header from '../header-edit-mode'; import SiteIcon from '../site-icon'; import SiteTitle from '../site-title'; +import useInitEditedEntityFromURL from '../sync-state-with-url/use-init-edited-entity-from-url'; const ANIMATION_DURATION = 0.5; export default function Layout( { onError } ) { + // This ensures the edited entity id and type are initialized properly. + useInitEditedEntityFromURL(); + const { params } = useLocation(); const isListPage = getIsListPage( params ); const isEditorPage = ! isListPage; - const { canvasMode, dashboardLink } = useSelect( - ( select ) => ( { - canvasMode: select( editSiteStore ).__unstableGetCanvasMode(), - dashboardLink: - select( editSiteStore ).getSettings() - .__experimentalDashboardLink, - } ), - [] - ); + const { canvasMode, dashboardLink, previousShortcut, nextShortcut } = + useSelect( ( select ) => { + const { getAllShortcutKeyCombinations } = select( + keyboardShortcutsStore + ); + const { __unstableGetCanvasMode, getSettings } = + select( editSiteStore ); + return { + canvasMode: __unstableGetCanvasMode(), + dashboardLink: getSettings().__experimentalDashboardLink, + previousShortcut: getAllShortcutKeyCombinations( + 'core/edit-site/previous-region' + ), + nextShortcut: getAllShortcutKeyCombinations( + 'core/edit-site/next-region' + ), + }; + }, [] ); + const navigateRegionsProps = useNavigateRegions( { + previous: previousShortcut, + next: nextShortcut, + } ); const { __unstableSetCanvasMode } = useDispatch( editSiteStore ); const { clearSelectedBlock } = useDispatch( blockEditorStore ); const disableMotion = useReducedMotion(); @@ -104,13 +123,19 @@ export default function Layout( { onError } ) { <> { fullResizer }
@@ -173,7 +198,7 @@ export default function Layout( { onError } ) { as={ motion.div } initial={ { y: -60 } } animate={ { y: 0 } } - edit={ { y: -60 } } + exit={ { y: -60 } } transition={ { type: 'tween', duration: disableMotion @@ -192,7 +217,8 @@ export default function Layout( { onError } ) { { showSidebar && ( - - + ) } diff --git a/packages/edit-site/src/components/layout/style.scss b/packages/edit-site/src/components/layout/style.scss index de34bc008f739..4e92e6c0a57fb 100644 --- a/packages/edit-site/src/components/layout/style.scss +++ b/packages/edit-site/src/components/layout/style.scss @@ -39,7 +39,8 @@ .edit-site-layout__edit-button { background: $gray-800; - color: $white; + /* Overrides the color for all states of the button */ + color: inherit !important; } .edit-site-layout__sidebar { diff --git a/packages/edit-site/src/components/list/header.js b/packages/edit-site/src/components/list/header.js index 91b9a13aeeb9c..5468c271fe628 100644 --- a/packages/edit-site/src/components/list/header.js +++ b/packages/edit-site/src/components/list/header.js @@ -36,7 +36,11 @@ export default function Header( { templateType } ) { { canCreate && (
- +
) } diff --git a/packages/edit-site/src/components/list/style.scss b/packages/edit-site/src/components/list/style.scss index b8c8f2be27db1..c3359b752fc05 100644 --- a/packages/edit-site/src/components/list/style.scss +++ b/packages/edit-site/src/components/list/style.scss @@ -44,18 +44,11 @@ .interface-interface-skeleton__content { background: $white; padding: $grid-unit-20; - - .interface-navigable-region__stacker { - align-items: center; - } + align-items: center; @include break-medium() { padding: $grid-unit * 9; } - - & > .interface-navigable-region__stacker { - height: auto; - } } } } diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-main/index.js b/packages/edit-site/src/components/sidebar-navigation-screen-main/index.js index 9a7b4479c972b..622eb70a951aa 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-main/index.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-main/index.js @@ -32,7 +32,7 @@ export default function SidebarNavigationScreenMain() { +
{ __( 'Design' ) }
{ ! isMobileViewport && isEditorPage && ( + +
+ { config[ postType ].labels.title } +
+ { ! isMobileViewport && ( + + + { isEditorPage && ( + + ) } + ) }
} diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-templates/style.scss b/packages/edit-site/src/components/sidebar-navigation-screen-templates/style.scss index 432774f903e46..174c7210e8403 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-templates/style.scss +++ b/packages/edit-site/src/components/sidebar-navigation-screen-templates/style.scss @@ -2,3 +2,8 @@ /* Overrides the margin that comes from the Item component */ margin-top: $grid-unit-20 !important; } + +.edit-site-sidebar-navigation-screen-templates__add-button { + /* Overrides the color for all states of the button */ + color: inherit !important; +} diff --git a/packages/edit-site/src/components/sidebar-navigation-screen/style.scss b/packages/edit-site/src/components/sidebar-navigation-screen/style.scss index e3044e95bbdd0..91c64bbd4c3ad 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen/style.scss +++ b/packages/edit-site/src/components/sidebar-navigation-screen/style.scss @@ -6,7 +6,7 @@ } .edit-site-sidebar-navigation-screen__content { - margin: 0 $button-size; + margin: 0 $grid-unit-20 0 $button-size; flex-grow: 1; overflow-y: auto; } @@ -19,11 +19,13 @@ box-shadow: 0 $grid-unit-10 $grid-unit-20 $gray-900; margin-bottom: $grid-unit-10; padding-bottom: $grid-unit-10; + padding-right: $grid-unit-20; } .edit-site-sidebar-navigation-screen__title { font-size: calc(1.56 * 13px); - font-weight: 600; + font-weight: 500; + flex-grow: 1; } .edit-site-sidebar-navigation-screen__back { diff --git a/packages/edit-site/src/hooks/index.js b/packages/edit-site/src/hooks/index.js index 1f7196dd2256c..513634c55b8f0 100644 --- a/packages/edit-site/src/hooks/index.js +++ b/packages/edit-site/src/hooks/index.js @@ -2,4 +2,5 @@ * Internal dependencies */ import './components'; +import './push-changes-to-global-styles'; import './template-part-edit'; diff --git a/packages/edit-site/src/hooks/push-changes-to-global-styles/index.js b/packages/edit-site/src/hooks/push-changes-to-global-styles/index.js new file mode 100644 index 0000000000000..70b9fa1b02f8e --- /dev/null +++ b/packages/edit-site/src/hooks/push-changes-to-global-styles/index.js @@ -0,0 +1,162 @@ +/** + * External dependencies + */ +import { get, set } from 'lodash'; + +/** + * WordPress dependencies + */ +import { addFilter } from '@wordpress/hooks'; +import { createHigherOrderComponent } from '@wordpress/compose'; +import { + InspectorAdvancedControls, + store as blockEditorStore, +} from '@wordpress/block-editor'; +import { BaseControl, Button } from '@wordpress/components'; +import { __, sprintf } from '@wordpress/i18n'; +import { + __EXPERIMENTAL_STYLE_PROPERTY as STYLE_PROPERTY, + getBlockType, +} from '@wordpress/blocks'; +import { useContext, useMemo, useCallback } from '@wordpress/element'; +import { useDispatch } from '@wordpress/data'; +import { store as noticesStore } from '@wordpress/notices'; + +/** + * Internal dependencies + */ +import { getSupportedGlobalStylesPanels } from '../../components/global-styles/hooks'; +import { GlobalStylesContext } from '../../components/global-styles/context'; +import { + STYLE_PATH_TO_CSS_VAR_INFIX, + STYLE_PATH_TO_PRESET_BLOCK_ATTRIBUTE, +} from '../../components/global-styles/utils'; + +function getChangesToPush( name, attributes ) { + return getSupportedGlobalStylesPanels( name ).flatMap( ( key ) => { + if ( ! STYLE_PROPERTY[ key ] ) { + return []; + } + const { value: path } = STYLE_PROPERTY[ key ]; + const presetAttributeKey = path.join( '.' ); + const presetAttributeValue = + attributes[ + STYLE_PATH_TO_PRESET_BLOCK_ATTRIBUTE[ presetAttributeKey ] + ]; + const value = presetAttributeValue + ? `var:preset|${ STYLE_PATH_TO_CSS_VAR_INFIX[ presetAttributeKey ] }|${ presetAttributeValue }` + : get( attributes.style, path ); + return value ? [ { path, value } ] : []; + } ); +} + +function cloneDeep( object ) { + return ! object ? {} : JSON.parse( JSON.stringify( object ) ); +} + +function PushChangesToGlobalStylesControl( { + name, + attributes, + setAttributes, +} ) { + const changes = useMemo( + () => getChangesToPush( name, attributes ), + [ name, attributes ] + ); + + const { user: userConfig, setUserConfig } = + useContext( GlobalStylesContext ); + + const { __unstableMarkNextChangeAsNotPersistent } = + useDispatch( blockEditorStore ); + const { createSuccessNotice } = useDispatch( noticesStore ); + + const pushChanges = useCallback( () => { + if ( changes.length === 0 ) { + return; + } + + const { style: blockStyles } = attributes; + + const newBlockStyles = cloneDeep( blockStyles ); + const newUserConfig = cloneDeep( userConfig ); + + for ( const { path, value } of changes ) { + set( newBlockStyles, path, undefined ); + set( newUserConfig, [ 'styles', 'blocks', name, ...path ], value ); + } + + // @wordpress/core-data doesn't support editing multiple entity types in + // a single undo level. So for now, we disable @wordpress/core-data undo + // tracking and implement our own Undo button in the snackbar + // notification. + __unstableMarkNextChangeAsNotPersistent(); + setAttributes( { style: newBlockStyles } ); + setUserConfig( () => newUserConfig, { undoIgnore: true } ); + + createSuccessNotice( + sprintf( + // translators: %s: Title of the block e.g. 'Heading'. + __( 'Pushed styles to all %s blocks.' ), + getBlockType( name ).title + ), + { + type: 'snackbar', + actions: [ + { + label: __( 'Undo' ), + onClick() { + __unstableMarkNextChangeAsNotPersistent(); + setAttributes( { style: blockStyles } ); + setUserConfig( () => userConfig, { + undoIgnore: true, + } ); + }, + }, + ], + } + ); + }, [ changes, attributes, userConfig, name ] ); + + return ( + + + { __( 'Styles' ) } + + + + ); +} + +const withPushChangesToGlobalStyles = createHigherOrderComponent( + ( BlockEdit ) => ( props ) => + ( + <> + + + + + + ) +); + +addFilter( + 'editor.BlockEdit', + 'core/edit-site/push-changes-to-global-styles', + withPushChangesToGlobalStyles +); diff --git a/packages/edit-site/src/hooks/push-changes-to-global-styles/style.scss b/packages/edit-site/src/hooks/push-changes-to-global-styles/style.scss new file mode 100644 index 0000000000000..33767f4879a40 --- /dev/null +++ b/packages/edit-site/src/hooks/push-changes-to-global-styles/style.scss @@ -0,0 +1,4 @@ +.edit-site-push-changes-to-global-styles-control .components-button { + justify-content: center; + width: 100%; +} diff --git a/packages/edit-site/src/index.js b/packages/edit-site/src/index.js index 4b619f7aac9a9..d2cbb2f4aa04d 100644 --- a/packages/edit-site/src/index.js +++ b/packages/edit-site/src/index.js @@ -16,6 +16,7 @@ import { store as editorStore } from '@wordpress/editor'; import { store as interfaceStore } from '@wordpress/interface'; import { store as preferencesStore } from '@wordpress/preferences'; import { addFilter } from '@wordpress/hooks'; +import { registerLegacyWidgetBlock } from '@wordpress/widgets'; /** * Internal dependencies @@ -109,6 +110,7 @@ export function initializeEditor( id, settings ) { dispatch( blocksStore ).__experimentalReapplyBlockTypeFilters(); registerCoreBlocks(); + registerLegacyWidgetBlock( { inserter: false } ); if ( process.env.IS_GUTENBERG_PLUGIN ) { __experimentalRegisterExperimentalCoreBlocks( { enableFSEBlocks: true, diff --git a/packages/edit-site/src/style.scss b/packages/edit-site/src/style.scss index be5b51e16ea08..8b37e2314e237 100644 --- a/packages/edit-site/src/style.scss +++ b/packages/edit-site/src/style.scss @@ -24,6 +24,7 @@ @import "./components/sidebar-navigation-screen-templates/style.scss"; @import "./components/site-icon/style.scss"; @import "./components/style-book/style.scss"; +@import "./hooks/push-changes-to-global-styles/style.scss"; html #wpadminbar { display: none; diff --git a/packages/edit-widgets/src/components/keyboard-shortcuts/index.js b/packages/edit-widgets/src/components/keyboard-shortcuts/index.js index dd039295c3661..65ecceeb4aa8c 100644 --- a/packages/edit-widgets/src/components/keyboard-shortcuts/index.js +++ b/packages/edit-widgets/src/components/keyboard-shortcuts/index.js @@ -6,6 +6,7 @@ import { useShortcut, store as keyboardShortcutsStore, } from '@wordpress/keyboard-shortcuts'; +import { isAppleOS } from '@wordpress/keycodes'; import { useDispatch } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; import { store as coreStore } from '@wordpress/core-data'; @@ -59,6 +60,18 @@ function KeyboardShortcutsRegister() { modifier: 'primaryShift', character: 'z', }, + // Disable on Apple OS because it conflicts with the browser's + // history shortcut. It's a fine alias for both Windows and Linux. + // Since there's no conflict for Ctrl+Shift+Z on both Windows and + // Linux, we keep it as the default for consistency. + aliases: isAppleOS() + ? [] + : [ + { + modifier: 'primary', + character: 'y', + }, + ], } ); registerShortcut( { diff --git a/packages/editor/src/components/page-attributes/parent.js b/packages/editor/src/components/page-attributes/parent.js index 4ee2491e17209..2e402f59415c7 100644 --- a/packages/editor/src/components/page-attributes/parent.js +++ b/packages/editor/src/components/page-attributes/parent.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { get, unescape as unescapeString, find } from 'lodash'; +import { get, unescape as unescapeString } from 'lodash'; import removeAccents from 'remove-accents'; /** @@ -122,8 +122,7 @@ export function PageAttributesParent() { const opts = getOptionsFromTree( tree ); // Ensure the current parent is in the options list. - const optsHasParent = find( - opts, + const optsHasParent = opts.find( ( item ) => item.value === parentPostId ); if ( parentPost && ! optsHasParent ) { diff --git a/packages/editor/src/components/post-format/index.js b/packages/editor/src/components/post-format/index.js index 4f3ec2c696693..518ec2c0d26bc 100644 --- a/packages/editor/src/components/post-format/index.js +++ b/packages/editor/src/components/post-format/index.js @@ -1,8 +1,3 @@ -/** - * External dependencies - */ -import { find } from 'lodash'; - /** * WordPress dependencies */ @@ -69,8 +64,7 @@ export default function PostFormat() { supportedFormats?.includes( format.id ) || postFormat === format.id ); } ); - const suggestion = find( - formats, + const suggestion = formats.find( ( format ) => format.id === suggestedFormat ); diff --git a/packages/editor/src/components/post-publish-panel/maybe-post-format-panel.js b/packages/editor/src/components/post-publish-panel/maybe-post-format-panel.js index e2aff854a7a9e..106665c9ea69e 100644 --- a/packages/editor/src/components/post-publish-panel/maybe-post-format-panel.js +++ b/packages/editor/src/components/post-publish-panel/maybe-post-format-panel.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { find, get } from 'lodash'; +import { get } from 'lodash'; /** * WordPress dependencies @@ -21,7 +21,7 @@ const getSuggestion = ( supportedFormats, suggestedPostFormat ) => { const formats = POST_FORMATS.filter( ( format ) => supportedFormats?.includes( format.id ) ); - return find( formats, ( format ) => format.id === suggestedPostFormat ); + return formats.find( ( format ) => format.id === suggestedPostFormat ); }; const PostFormatSuggestion = ( { 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 2195628cdd168..b47e7bd3c9dda 100644 --- a/packages/editor/src/components/post-taxonomies/flat-term-selector.js +++ b/packages/editor/src/components/post-taxonomies/flat-term-selector.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { find, get } from 'lodash'; +import { get } from 'lodash'; import escapeHtml from 'escape-html'; /** @@ -50,7 +50,7 @@ const isSameTermName = ( termA, termB ) => const termNamesToIds = ( names, terms ) => { return names.map( ( termName ) => - find( terms, ( term ) => isSameTermName( term.name, termName ) ).id + terms.find( ( term ) => isSameTermName( term.name, termName ) ).id ); }; @@ -203,7 +203,7 @@ export function FlatTermSelector( { slug } ) { const newTermNames = uniqueTerms.filter( ( termName ) => - ! find( availableTerms, ( term ) => + ! availableTerms.find( ( term ) => isSameTermName( term.name, termName ) ) ); 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 7663113dc0320..58734bea26789 100644 --- a/packages/editor/src/components/post-taxonomies/hierarchical-term-selector.js +++ b/packages/editor/src/components/post-taxonomies/hierarchical-term-selector.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { find, get, unescape as unescapeString } from 'lodash'; +import { get, unescape as unescapeString } from 'lodash'; /** * WordPress dependencies @@ -95,7 +95,7 @@ export function sortBySelected( termsTree, terms ) { * @return {Object} Term object. */ export function findTerm( terms, parent, name ) { - return find( terms, ( term ) => { + return terms.find( ( term ) => { return ( ( ( ! term.parent && ! parent ) || parseInt( term.parent ) === parseInt( parent ) ) && diff --git a/packages/icons/src/library/update.js b/packages/icons/src/library/update.js index d751fb970ea55..09a291c7e9756 100644 --- a/packages/icons/src/library/update.js +++ b/packages/icons/src/library/update.js @@ -4,8 +4,8 @@ import { SVG, Path } from '@wordpress/primitives'; const update = ( - - + + ); diff --git a/packages/interface/src/components/interface-skeleton/index.js b/packages/interface/src/components/interface-skeleton/index.js index e8a7a45ede3fa..fe329a75d43e0 100644 --- a/packages/interface/src/components/interface-skeleton/index.js +++ b/packages/interface/src/components/interface-skeleton/index.js @@ -46,6 +46,7 @@ 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, @@ -83,8 +84,11 @@ function InterfaceSkeleton( return (
-
- { children } -
+ { children } ); } diff --git a/packages/nux/src/components/dot-tip/test/__snapshots__/index.js.snap b/packages/nux/src/components/dot-tip/test/__snapshots__/index.js.snap index 876bae4b69088..a453da0254b48 100644 --- a/packages/nux/src/components/dot-tip/test/__snapshots__/index.js.snap +++ b/packages/nux/src/components/dot-tip/test/__snapshots__/index.js.snap @@ -5,7 +5,7 @@ exports[`DotTip should render correctly 1`] = ` aria-label="Editor tips" class="components-popover nux-dot-tip is-positioned" role="dialog" - style="position: absolute; opacity: 0; transform: translateX(-2em) scale(0) translateZ(0); transform-origin: 0% 50% 0; left: 0px; top: 0px;" + style="position: absolute; top: 0px; left: 0px; opacity: 0; transform: translateX(-2em) scale(0) translateZ(0); transform-origin: 0% 50% 0;" tabindex="-1" >
{ +// Disabled for now on Android see https://github.com/wordpress-mobile/gutenberg-mobile/issues/5321 +const onlyOniOS = ! isAndroid() ? describe : describe.skip; + +onlyOniOS( 'Gutenberg Editor Unsupported Block Editor Tests', () => { it( 'should be able to open the unsupported block web view editor', async () => { await editorPage.setHtmlContent( testData.unsupportedBlockHtml ); diff --git a/packages/style-engine/class-wp-style-engine.php b/packages/style-engine/class-wp-style-engine.php index f5636c150c3ee..0fd0aeac60e7c 100644 --- a/packages/style-engine/class-wp-style-engine.php +++ b/packages/style-engine/class-wp-style-engine.php @@ -206,6 +206,12 @@ final class WP_Style_Engine { ), 'path' => array( 'typography', 'lineHeight' ), ), + 'textColumns' => array( + 'property_keys' => array( + 'default' => 'column-count', + ), + 'path' => array( 'typography', 'textColumns' ), + ), 'textDecoration' => array( 'property_keys' => array( 'default' => 'text-decoration', diff --git a/packages/style-engine/src/styles/typography/index.ts b/packages/style-engine/src/styles/typography/index.ts index 48effa0b2b8a9..75e02c9643051 100644 --- a/packages/style-engine/src/styles/typography/index.ts +++ b/packages/style-engine/src/styles/typography/index.ts @@ -76,6 +76,18 @@ const lineHeight = { }, }; +const textColumns = { + name: 'textColumns', + generate: ( style: Style, options: StyleOptions ) => { + return generateRule( + style, + options, + [ 'typography', 'textColumns' ], + 'columnCount' + ); + }, +}; + const textDecoration = { name: 'textDecoration', generate: ( style: Style, options: StyleOptions ) => { @@ -107,6 +119,7 @@ export default [ fontWeight, letterSpacing, lineHeight, + textColumns, textDecoration, textTransform, ]; diff --git a/packages/style-engine/src/test/index.js b/packages/style-engine/src/test/index.js index a94bd8a425681..fac55b4000e58 100644 --- a/packages/style-engine/src/test/index.js +++ b/packages/style-engine/src/test/index.js @@ -72,6 +72,7 @@ describe( 'generate', () => { fontWeight: '800', fontFamily: "'Helvetica Neue',sans-serif", lineHeight: '3.3', + textColumns: '2', textDecoration: 'line-through', letterSpacing: '12px', textTransform: 'uppercase', @@ -88,7 +89,7 @@ describe( 'generate', () => { } ) ).toEqual( - ".some-selector { color: #cccccc; background: linear-gradient(135deg,rgb(255,203,112) 0%,rgb(33,32,33) 42%,rgb(65,88,208) 100%); background-color: #111111; min-height: 50vh; outline-color: red; outline-style: dashed; outline-offset: 2px; outline-width: 4px; margin-top: 11px; margin-right: 12px; margin-bottom: 13px; margin-left: 14px; padding-top: 10px; padding-bottom: 5px; font-family: 'Helvetica Neue',sans-serif; font-size: 2.2rem; font-style: italic; font-weight: 800; letter-spacing: 12px; line-height: 3.3; text-decoration: line-through; text-transform: uppercase; }" + ".some-selector { color: #cccccc; background: linear-gradient(135deg,rgb(255,203,112) 0%,rgb(33,32,33) 42%,rgb(65,88,208) 100%); background-color: #111111; min-height: 50vh; outline-color: red; outline-style: dashed; outline-offset: 2px; outline-width: 4px; margin-top: 11px; margin-right: 12px; margin-bottom: 13px; margin-left: 14px; padding-top: 10px; padding-bottom: 5px; font-family: 'Helvetica Neue',sans-serif; font-size: 2.2rem; font-style: italic; font-weight: 800; letter-spacing: 12px; line-height: 3.3; column-count: 2; text-decoration: line-through; text-transform: uppercase; }" ); } ); @@ -242,6 +243,7 @@ describe( 'getCSSRules', () => { fontWeight: '800', fontFamily: "'Helvetica Neue',sans-serif", lineHeight: '3.3', + textColumns: '2', textDecoration: 'line-through', letterSpacing: '12px', textTransform: 'uppercase', @@ -349,6 +351,11 @@ describe( 'getCSSRules', () => { key: 'lineHeight', value: '3.3', }, + { + selector: '.some-selector', + key: 'columnCount', + value: '2', + }, { selector: '.some-selector', key: 'textDecoration', diff --git a/packages/style-engine/src/types.ts b/packages/style-engine/src/types.ts index 0063cd56621c6..23d3e38cc43c2 100644 --- a/packages/style-engine/src/types.ts +++ b/packages/style-engine/src/types.ts @@ -52,6 +52,7 @@ export interface Style { fontStyle?: CSSProperties[ 'fontStyle' ]; letterSpacing?: CSSProperties[ 'letterSpacing' ]; lineHeight?: CSSProperties[ 'lineHeight' ]; + textColumns?: CSSProperties[ 'columnCount' ]; textDecoration?: CSSProperties[ 'textDecoration' ]; textTransform?: CSSProperties[ 'textTransform' ]; }; diff --git a/packages/widgets/src/index.js b/packages/widgets/src/index.js index 6c88d65556d8b..e55b16ff12b35 100644 --- a/packages/widgets/src/index.js +++ b/packages/widgets/src/index.js @@ -18,11 +18,21 @@ export * from './utils'; * Note that for the block to be useful, any scripts required by a widget must * be loaded into the page. * + * @param {Object} supports Block support settings. * @see https://developer.wordpress.org/block-editor/how-to-guides/widgets/legacy-widget-block/ */ -export function registerLegacyWidgetBlock() { +export function registerLegacyWidgetBlock( supports = {} ) { const { metadata, settings, name } = legacyWidget; - registerBlockType( { name, ...metadata }, settings ); + registerBlockType( + { name, ...metadata }, + { + ...settings, + supports: { + ...settings.supports, + ...supports, + }, + } + ); } /** diff --git a/phpunit/block-supports/typography-test.php b/phpunit/block-supports/typography-test.php index 63fd98d1e524f..1b9b6f97c63f6 100644 --- a/phpunit/block-supports/typography-test.php +++ b/phpunit/block-supports/typography-test.php @@ -557,22 +557,18 @@ public function data_generate_font_size_preset_fixtures() { /** * Tests that custom font sizes are converted to fluid values * in inline block supports styles, - * when "settings.typography.fluid" is set to `true`. + * when "settings.typography.fluid" is set to `true` or contains configured values. * * @covers ::gutenberg_register_typography_support * * @dataProvider data_generate_block_supports_font_size_fixtures * - * @param string $font_size_value The block supports custom font size value. - * @param bool $should_use_fluid_typography An override to switch fluid typography "on". Can be used for unit testing. - * @param string $expected_output Expected value of style property from gutenberg_apply_typography_support(). + * @param string $font_size_value The block supports custom font size value. + * @param string $theme_slug A theme slug corresponding to an available test theme. + * @param string $expected_output Expected value of style property from gutenberg_apply_typography_support(). */ - public function test_should_covert_font_sizes_to_fluid_values( $font_size_value, $should_use_fluid_typography, $expected_output ) { - if ( $should_use_fluid_typography ) { - switch_theme( 'block-theme-child-with-fluid-typography' ); - } else { - switch_theme( 'default' ); - } + public function test_should_covert_font_sizes_to_fluid_values( $font_size_value, $theme_slug, $expected_output ) { + switch_theme( $theme_slug ); $this->test_block_name = 'test/font-size-fluid-value'; register_block_type( @@ -614,15 +610,30 @@ public function test_should_covert_font_sizes_to_fluid_values( $font_size_value, */ public function data_generate_block_supports_font_size_fixtures() { return array( - 'default_return_value' => array( - 'font_size_value' => '50px', - 'should_use_fluid_typography' => false, - 'expected_output' => 'font-size:50px;', - ), - 'return_value_with_fluid_typography' => array( - 'font_size_value' => '50px', - 'should_use_fluid_typography' => true, - 'expected_output' => 'font-size:clamp(37.5px, 2.344rem + ((1vw - 7.68px) * 1.502), 50px);', + 'returns value when fluid typography is not active' => array( + 'font_size_value' => '15px', + 'theme_slug' => 'default', + 'expected_output' => 'font-size:15px;', + ), + 'returns clamp value using default config' => array( + 'font_size_value' => '15px', + 'theme_slug' => 'block-theme-child-with-fluid-typography', + 'expected_output' => 'font-size:clamp(14px, 0.875rem + ((1vw - 7.68px) * 0.12), 15px);', + ), + 'returns value when font size <= default min font size bound' => array( + 'font_size_value' => '13px', + 'theme_slug' => 'block-theme-child-with-fluid-typography', + 'expected_output' => 'font-size:13px;', + ), + 'returns clamp value using custom fluid config' => array( + 'font_size_value' => '17px', + 'theme_slug' => 'block-theme-child-with-fluid-typography-config', + 'expected_output' => 'font-size:clamp(16px, 1rem + ((1vw - 7.68px) * 0.12), 17px);', + ), + 'returns value when font size <= custom min font size bound' => array( + 'font_size_value' => '15px', + 'theme_slug' => 'block-theme-child-with-fluid-typography-config', + 'expected_output' => 'font-size:15px;', ), ); } diff --git a/phpunit/class-gutenberg-rest-block-patterns-controller-test.php b/phpunit/class-gutenberg-rest-block-patterns-controller-test.php index ad6f42b033e84..9f9cf60f0572a 100644 --- a/phpunit/class-gutenberg-rest-block-patterns-controller-test.php +++ b/phpunit/class-gutenberg-rest-block-patterns-controller-test.php @@ -10,27 +10,54 @@ * Unit tests for REST API for Block Patterns. * * @group restapi - * @covers Gutenberg_REST_Block_Patterns_Controller + * @covers Gutenberg_REST_Block_Patterns_Controller_6_2 */ -class Gutenberg_REST_Block_Patterns_Controller_Test extends WP_Test_REST_Controller_Testcase { +class Gutenberg_REST_Block_Patterns_Controller_6_2_Test extends WP_Test_REST_Controller_Testcase { + /** + * Admin user ID. + * + * @since 6.0.0 + * + * @var int + */ protected static $admin_id; + + /** + * Original instance of WP_Block_Patterns_Registry. + * + * @since 6.0.0 + * + * @var WP_Block_Patterns_Registry + */ protected static $orig_registry; - public function set_up() { - parent::set_up(); - switch_theme( 'emptytheme' ); - } + /** + * Instance of the reflected `instance` property. + * + * @since 6.0.0 + * + * @var ReflectionProperty + */ + private static $registry_instance_property; + + /** + * The REST API route. + * + * @since 6.0.0 + * + * @var string + */ + const REQUEST_ROUTE = '/wp/v2/block-patterns/patterns'; public static function wpSetUpBeforeClass( $factory ) { - // Create a test user. self::$admin_id = $factory->user->create( array( 'role' => 'administrator' ) ); // Setup an empty testing instance of `WP_Block_Patterns_Registry` and save the original. - $reflection = new ReflectionClass( 'WP_Block_Patterns_Registry' ); - $reflection->getProperty( 'instance' )->setAccessible( true ); - self::$orig_registry = $reflection->getStaticPropertyValue( 'instance' ); - $test_registry = new WP_Block_Patterns_Registry(); - $reflection->setStaticPropertyValue( 'instance', $test_registry ); + self::$orig_registry = WP_Block_Patterns_Registry::get_instance(); + self::$registry_instance_property = new ReflectionProperty( 'WP_Block_Patterns_Registry', 'instance' ); + self::$registry_instance_property->setAccessible( true ); + $test_registry = new WP_Block_Pattern_Categories_Registry(); + self::$registry_instance_property->setValue( $test_registry ); // Register some patterns in the test registry. $test_registry->register( @@ -51,47 +78,77 @@ public static function wpSetUpBeforeClass( $factory ) { 'content' => '

Two

', ) ); + + $test_registry->register( + 'test/three', + array( + 'title' => 'Pattern Three', + 'categories' => array( 'test', 'buttons', 'query' ), + 'content' => '

Three

', + ) + ); } public static function wpTearDownAfterClass() { - // Delete the test user. self::delete_user( self::$admin_id ); // Restore the original registry instance. - $reflection = new ReflectionClass( 'WP_Block_Patterns_Registry' ); - $reflection->setStaticPropertyValue( 'instance', self::$orig_registry ); + self::$registry_instance_property->setValue( self::$orig_registry ); + self::$registry_instance_property->setAccessible( false ); + self::$registry_instance_property = null; + self::$orig_registry = null; } - public function test_register_routes() { - $routes = rest_get_server()->get_routes(); - $this->assertArrayHasKey( - '/wp/v2/block-patterns/patterns', - $routes, - 'The patterns route does not exist' - ); + public function set_up() { + parent::set_up(); + switch_theme( 'emptytheme' ); } - public function test_get_items() { + public function test_get_items_migrate_pattern_categories() { wp_set_current_user( self::$admin_id ); - $expected_names = array( 'test/one', 'test/two' ); - $expected_fields = array( 'name', 'content' ); - - $request = new WP_REST_Request( 'GET', '/wp/v2/block-patterns/patterns' ); - $request['_fields'] = 'name,content'; + $request = new WP_REST_Request( 'GET', static::REQUEST_ROUTE ); + $request['_fields'] = 'name,categories'; $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); - $this->assertCount( count( $expected_names ), $data ); - foreach ( $data as $idx => $item ) { - $this->assertEquals( $expected_names[ $idx ], $item['name'] ); - $this->assertEquals( $expected_fields, array_keys( $item ) ); - } + $this->assertIsArray( $data, 'WP_REST_Block_Patterns_Controller::get_items() should return an array' ); + $this->assertGreaterThanOrEqual( 3, count( $data ), 'WP_REST_Block_Patterns_Controller::get_items() should return at least 3 items' ); + $this->assertSame( + array( + 'name' => 'test/one', + 'categories' => array( 'test' ), + ), + $data[0], + 'WP_REST_Block_Patterns_Controller::get_items() should return test/one' + ); + $this->assertSame( + array( + 'name' => 'test/two', + 'categories' => array( 'test' ), + ), + $data[1], + 'WP_REST_Block_Patterns_Controller::get_items() should return test/two' + ); + $this->assertSame( + array( + 'name' => 'test/three', + 'categories' => array( 'test', 'call-to-action', 'posts' ), + ), + $data[2], + 'WP_REST_Block_Patterns_Controller::get_items() should return test/three' + ); } /** * Abstract methods that we must implement. */ + public function test_register_routes() { + $this->markTestIncomplete(); + } + public function test_get_items() { + $this->markTestIncomplete(); + } public function test_context_param() { $this->markTestIncomplete(); } diff --git a/phpunit/class-wp-theme-json-test.php b/phpunit/class-wp-theme-json-test.php index 15932f7398bfa..a5413766ca7dd 100644 --- a/phpunit/class-wp-theme-json-test.php +++ b/phpunit/class-wp-theme-json-test.php @@ -751,6 +751,50 @@ public function test_get_stylesheet_with_block_support_feature_level_selectors() $this->assertEquals( $expected, $theme_json->get_stylesheet() ); } + public function test_allow_indirect_properties() { + $actual = WP_Theme_JSON_Gutenberg::remove_insecure_properties( + array( + 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, + 'styles' => array( + 'blocks' => array( + 'core/social-links' => array( + 'spacing' => array( + 'blockGap' => array( + 'top' => '1em', + 'left' => '2em', + ), + ), + ), + ), + 'spacing' => array( + 'blockGap' => '3em', + ), + ), + ) + ); + + $expected = array( + 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, + 'styles' => array( + 'blocks' => array( + 'core/social-links' => array( + 'spacing' => array( + 'blockGap' => array( + 'top' => '1em', + 'left' => '2em', + ), + ), + ), + ), + 'spacing' => array( + 'blockGap' => '3em', + ), + ), + ); + + $this->assertEqualSetsWithIndex( $expected, $actual ); + } + public function test_remove_invalid_element_pseudo_selectors() { $actual = WP_Theme_JSON_Gutenberg::remove_insecure_properties( array( @@ -1453,4 +1497,18 @@ public function test_update_separator_declarations() { $this->assertEquals( $expected, $stylesheet ); } + + public function test_get_stylesheet_handles_custom_css() { + $theme_json = new WP_Theme_JSON_Gutenberg( + array( + 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, + 'styles' => array( + 'css' => 'body { color:purple; }', + ), + ) + ); + + $custom_css = 'body { color:purple; }'; + $this->assertEquals( $custom_css, $theme_json->get_stylesheet( array( 'custom-css' ) ) ); + } } diff --git a/phpunit/data/themedir1/block-theme-child-with-fluid-typography-config/style.css b/phpunit/data/themedir1/block-theme-child-with-fluid-typography-config/style.css new file mode 100644 index 0000000000000..19abbecf86f4c --- /dev/null +++ b/phpunit/data/themedir1/block-theme-child-with-fluid-typography-config/style.css @@ -0,0 +1,8 @@ +/* +Theme Name: Block Theme Child Theme With Fluid Typography +Theme URI: https://wordpress.org/ +Description: For testing purposes only. +Template: block-theme +Version: 1.0.0 +Text Domain: block-theme-child-with-fluid-typography +*/ diff --git a/phpunit/data/themedir1/block-theme-child-with-fluid-typography-config/theme.json b/phpunit/data/themedir1/block-theme-child-with-fluid-typography-config/theme.json new file mode 100644 index 0000000000000..d0ec32d9caac0 --- /dev/null +++ b/phpunit/data/themedir1/block-theme-child-with-fluid-typography-config/theme.json @@ -0,0 +1,11 @@ +{ + "version": 2, + "settings": { + "appearanceTools": true, + "typography": { + "fluid": { + "minFontSize": "16px" + } + } + } +} diff --git a/phpunit/style-engine/style-engine-test.php b/phpunit/style-engine/style-engine-test.php index 66d8dd6286527..0588f10ef4fce 100644 --- a/phpunit/style-engine/style-engine-test.php +++ b/phpunit/style-engine/style-engine-test.php @@ -188,6 +188,7 @@ public function data_wp_style_engine_get_styles() { 'fontStyle' => 'italic', 'fontWeight' => '800', 'lineHeight' => '1.3', + 'textColumns' => '2', 'textDecoration' => 'underline', 'textTransform' => 'uppercase', 'letterSpacing' => '2', @@ -195,13 +196,14 @@ public function data_wp_style_engine_get_styles() { ), 'options' => null, 'expected_output' => array( - 'css' => 'font-size:clamp(2em, 2vw, 4em);font-family:Roboto,Oxygen-Sans,Ubuntu,sans-serif;font-style:italic;font-weight:800;line-height:1.3;text-decoration:underline;text-transform:uppercase;letter-spacing:2;', + 'css' => 'font-size:clamp(2em, 2vw, 4em);font-family:Roboto,Oxygen-Sans,Ubuntu,sans-serif;font-style:italic;font-weight:800;line-height:1.3;column-count:2;text-decoration:underline;text-transform:uppercase;letter-spacing:2;', 'declarations' => array( 'font-size' => 'clamp(2em, 2vw, 4em)', 'font-family' => 'Roboto,Oxygen-Sans,Ubuntu,sans-serif', 'font-style' => 'italic', 'font-weight' => '800', 'line-height' => '1.3', + 'column-count' => '2', 'text-decoration' => 'underline', 'text-transform' => 'uppercase', 'letter-spacing' => '2', diff --git a/schemas/json/theme.json b/schemas/json/theme.json index 1a8aa67b0967b..d734926219308 100644 --- a/schemas/json/theme.json +++ b/schemas/json/theme.json @@ -353,8 +353,23 @@ "default": true }, "fluid": { - "description": "Opts into fluid typography.", - "type": "boolean" + "description": "Enables fluid typography and allows users to set global fluid typography parameters.", + "oneOf": [ + { + "type": "object", + "properties": { + "minFontSize": { + "description": "Allow users to set a global minimum font size boundary in px, rem or em. Custom font sizes below this value will not be clamped, and all calculated minimum font sizes will be, a at minimum, this value.", + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "boolean" + } + ], + "default": false }, "letterSpacing": { "description": "Allow users to set custom letter spacing.", diff --git a/test/e2e/specs/editor/blocks/paragraph.spec.js b/test/e2e/specs/editor/blocks/paragraph.spec.js index c713379569b90..19c386cce457d 100644 --- a/test/e2e/specs/editor/blocks/paragraph.spec.js +++ b/test/e2e/specs/editor/blocks/paragraph.spec.js @@ -195,8 +195,12 @@ test.describe( 'Paragraph', () => { ); await expect( draggingUtils.dropZone ).toBeVisible(); await expect - .poll( () => draggingUtils.dropZone.boundingBox() ) - .toEqual( firstBlockBox ); + .poll( () => + draggingUtils.confirmValidDropZonePosition( + firstBlockBox + ) + ) + .toBeTruthy(); } { @@ -207,8 +211,12 @@ test.describe( 'Paragraph', () => { ); await expect( draggingUtils.dropZone ).toBeVisible(); await expect - .poll( () => draggingUtils.dropZone.boundingBox() ) - .toEqual( firstBlockBox ); + .poll( () => + draggingUtils.confirmValidDropZonePosition( + firstBlockBox + ) + ) + .toBeTruthy(); } { @@ -219,8 +227,12 @@ test.describe( 'Paragraph', () => { ); await expect( draggingUtils.dropZone ).toBeVisible(); await expect - .poll( () => draggingUtils.dropZone.boundingBox() ) - .toEqual( firstBlockBox ); + .poll( () => + draggingUtils.confirmValidDropZonePosition( + firstBlockBox + ) + ) + .toBeTruthy(); } { @@ -231,8 +243,12 @@ test.describe( 'Paragraph', () => { ); await expect( draggingUtils.dropZone ).toBeVisible(); await expect - .poll( () => draggingUtils.dropZone.boundingBox() ) - .toEqual( firstBlockBox ); + .poll( () => + draggingUtils.confirmValidDropZonePosition( + firstBlockBox + ) + ) + .toBeTruthy(); } { @@ -309,8 +325,12 @@ test.describe( 'Paragraph', () => { ); await expect( draggingUtils.dropZone ).toBeVisible(); await expect - .poll( () => draggingUtils.dropZone.boundingBox() ) - .toEqual( secondBlockBox ); + .poll( () => + draggingUtils.confirmValidDropZonePosition( + secondBlockBox + ) + ) + .toBeTruthy(); } { @@ -321,8 +341,12 @@ test.describe( 'Paragraph', () => { ); await expect( draggingUtils.dropZone ).toBeVisible(); await expect - .poll( () => draggingUtils.dropZone.boundingBox() ) - .toEqual( secondBlockBox ); + .poll( () => + draggingUtils.confirmValidDropZonePosition( + secondBlockBox + ) + ) + .toBeTruthy(); } { @@ -333,8 +357,12 @@ test.describe( 'Paragraph', () => { ); await expect( draggingUtils.dropZone ).toBeVisible(); await expect - .poll( () => draggingUtils.dropZone.boundingBox() ) - .toEqual( secondBlockBox ); + .poll( () => + draggingUtils.confirmValidDropZonePosition( + secondBlockBox + ) + ) + .toBeTruthy(); } { @@ -345,8 +373,12 @@ test.describe( 'Paragraph', () => { ); await expect( draggingUtils.dropZone ).toBeVisible(); await expect - .poll( () => draggingUtils.dropZone.boundingBox() ) - .toEqual( secondBlockBox ); + .poll( () => + draggingUtils.confirmValidDropZonePosition( + secondBlockBox + ) + ) + .toBeTruthy(); } } ); @@ -387,8 +419,12 @@ test.describe( 'Paragraph', () => { ); await expect( draggingUtils.dropZone ).toBeVisible(); await expect - .poll( () => draggingUtils.dropZone.boundingBox() ) - .toEqual( firstBlockBox ); + .poll( () => + draggingUtils.confirmValidDropZonePosition( + firstBlockBox + ) + ) + .toBeTruthy(); } { @@ -399,8 +435,12 @@ test.describe( 'Paragraph', () => { ); await expect( draggingUtils.dropZone ).toBeVisible(); await expect - .poll( () => draggingUtils.dropZone.boundingBox() ) - .toEqual( firstBlockBox ); + .poll( () => + draggingUtils.confirmValidDropZonePosition( + firstBlockBox + ) + ) + .toBeTruthy(); } { @@ -411,8 +451,12 @@ test.describe( 'Paragraph', () => { ); await expect( draggingUtils.dropZone ).toBeVisible(); await expect - .poll( () => draggingUtils.dropZone.boundingBox() ) - .toEqual( firstBlockBox ); + .poll( () => + draggingUtils.confirmValidDropZonePosition( + firstBlockBox + ) + ) + .toBeTruthy(); } { @@ -423,8 +467,12 @@ test.describe( 'Paragraph', () => { ); await expect( draggingUtils.dropZone ).toBeVisible(); await expect - .poll( () => draggingUtils.dropZone.boundingBox() ) - .toEqual( secondBlockBox ); + .poll( () => + draggingUtils.confirmValidDropZonePosition( + secondBlockBox + ) + ) + .toBeTruthy(); } { @@ -435,8 +483,12 @@ test.describe( 'Paragraph', () => { ); await expect( draggingUtils.dropZone ).toBeVisible(); await expect - .poll( () => draggingUtils.dropZone.boundingBox() ) - .toEqual( secondBlockBox ); + .poll( () => + draggingUtils.confirmValidDropZonePosition( + secondBlockBox + ) + ) + .toBeTruthy(); } { @@ -447,8 +499,12 @@ test.describe( 'Paragraph', () => { ); await expect( draggingUtils.dropZone ).toBeVisible(); await expect - .poll( () => draggingUtils.dropZone.boundingBox() ) - .toEqual( secondBlockBox ); + .poll( () => + draggingUtils.confirmValidDropZonePosition( + secondBlockBox + ) + ) + .toBeTruthy(); } } ); } ); @@ -508,4 +564,14 @@ class DraggingUtils { await this.page.mouse.move( 0, 0 ); await this.page.mouse.down(); } + + async confirmValidDropZonePosition( element ) { + // Check that both x and y axis of the dropzone + // have a less than 1 difference with a given target element + const box = await this.dropZone.boundingBox(); + return ( + Math.abs( element.x - box.x ) < 1 && + Math.abs( element.y - box.y ) < 1 + ); + } } diff --git a/test/e2e/specs/editor/blocks/quote.spec.js b/test/e2e/specs/editor/blocks/quote.spec.js index 69d69f0003489..299d56785d831 100644 --- a/test/e2e/specs/editor/blocks/quote.spec.js +++ b/test/e2e/specs/editor/blocks/quote.spec.js @@ -3,29 +3,317 @@ */ const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); -test.describe( 'adding a quote', () => { +test.describe( 'Quote', () => { + test.beforeEach( async ( { admin } ) => { + await admin.createNewPost(); + } ); + + test.afterEach( async ( { requestUtils } ) => { + await requestUtils.deleteAllPosts(); + } ); + test( 'should allow the user to type right away', async ( { - admin, editor, page, } ) => { - await admin.createNewPost(); + await editor.insertBlock( { name: 'core/quote' } ); + // Type content right after. + await page.keyboard.type( 'Quote content' ); + expect( await editor.getEditedPostContent() ).toBe( + ` +
+

Quote content

+
+` + ); + } ); + + test( 'can be created by using > at the start of a paragraph block', async ( { + editor, + page, + } ) => { + // Create a block with some text that will trigger a paragraph creation. + await page.click( 'role=button[name="Add default block"i]' ); + await page.keyboard.type( '> A quote' ); + // Create a second paragraph. + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( 'Another paragraph' ); + expect( await editor.getEditedPostContent() ).toBe( + ` +
+

A quote

+ + + +

Another paragraph

+
+` + ); + } ); + + test( 'can be created by typing > in front of text of a paragraph block', async ( { + editor, + page, + pageUtils, + } ) => { + await page.click( 'role=button[name="Add default block"i]' ); + await page.keyboard.type( 'test' ); + await pageUtils.pressKeyTimes( 'ArrowLeft', 'test'.length ); + await page.keyboard.type( '> ' ); + expect( await editor.getEditedPostContent() ).toBe( + ` +
+

test

+
+` + ); + } ); + + test( 'can be created by typing "/quote"', async ( { editor, page } ) => { + // Create a list with the slash block shortcut. + await page.click( 'role=button[name="Add default block"i]' ); + await page.keyboard.type( '/quote' ); + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( 'I’m a quote' ); + expect( await editor.getEditedPostContent() ).toBe( + ` +
+

I’m a quote

+
+` + ); + } ); + + test( 'can be created by converting a paragraph', async ( { + editor, + page, + } ) => { + await page.click( 'role=button[name="Add default block"i]' ); + await page.keyboard.type( 'test' ); + await editor.transformBlockTo( 'core/quote' ); + expect( await editor.getEditedPostContent() ).toBe( + ` +
+

test

+
+` + ); + } ); + + test( 'can be created by converting multiple paragraphs', async ( { + editor, + page, + } ) => { + await page.click( 'role=button[name="Add default block"i]' ); + await page.keyboard.type( 'one' ); + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( 'two' ); + await page.keyboard.down( 'Shift' ); + await page.click( + 'role=document[name="Paragraph block"i] >> text=one' + ); + await page.keyboard.up( 'Shift' ); + await editor.transformBlockTo( 'core/quote' ); + expect( await editor.getEditedPostContent() ).toBe( + ` +
+

one

+ + + +

two

+
+` + ); + } ); - // Inserting a quote block - await editor.insertBlock( { - name: 'core/quote', + test.describe( 'can be converted to paragraphs', () => { + test( 'and renders one paragraph block per

within quote', async ( { + editor, + page, + } ) => { + await editor.insertBlock( { name: 'core/quote' } ); + await page.keyboard.type( 'one' ); + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( 'two' ); + // Navigate to the citation to select the block. + await page.keyboard.press( 'ArrowRight' ); + // Unwrap the block. + await editor.transformBlockTo( '*' ); + expect( await editor.getEditedPostContent() ).toBe( + ` +

one

+ + + +

two

+` + ); } ); - // Type content right after. - await page.keyboard.type( 'Quote content' ); + test( 'and renders a paragraph for the cite, if it exists', async ( { + editor, + page, + } ) => { + await editor.insertBlock( { name: 'core/quote' } ); + await page.keyboard.type( 'one' ); + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( 'two' ); + await page.keyboard.press( 'ArrowRight' ); + await page.keyboard.type( 'cite' ); + // Unwrap the block. + await editor.transformBlockTo( '*' ); + expect( await editor.getEditedPostContent() ).toBe( + ` +

one

+ + + +

two

+ + + +

cite

+` + ); + } ); + + test( 'and renders only one paragraph for the cite, if the quote is void', async ( { + editor, + page, + } ) => { + await editor.insertBlock( { name: 'core/quote' } ); + await page.keyboard.press( 'ArrowRight' ); + await page.keyboard.type( 'cite' ); + // Unwrap the block. + await editor.transformBlockTo( '*' ); + expect( await editor.getEditedPostContent() ).toBe( + ` +

+ - // Check the content - const content = await editor.getEditedPostContent(); - expect( content ).toBe( + +

cite

+` + ); + } ); + + test( 'and renders a void paragraph if both the cite and quote are void', async ( { + editor, + page, + } ) => { + await editor.insertBlock( { name: 'core/quote' } ); + // Select the quote + await page.keyboard.press( 'ArrowRight' ); + // Unwrap the block. + await editor.transformBlockTo( '*' ); + expect( await editor.getEditedPostContent() ).toBe( '' ); + } ); + } ); + + test( 'can be created by converting a heading', async ( { + editor, + page, + } ) => { + await editor.insertBlock( { name: 'core/heading' } ); + await page.keyboard.type( 'test' ); + await editor.transformBlockTo( 'core/quote' ); + expect( await editor.getEditedPostContent() ).toBe( + ` +
+

test

+
+` + ); + } ); + + test( 'can be converted to a pullquote', async ( { editor, page } ) => { + await editor.insertBlock( { name: 'core/quote' } ); + await page.keyboard.type( 'one' ); + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( 'two' ); + await page.keyboard.press( 'ArrowRight' ); + await page.keyboard.type( 'cite' ); + await editor.transformBlockTo( 'core/pullquote' ); + expect( await editor.getEditedPostContent() ).toBe( + ` +

one
two

cite
+` + ); + } ); + + test( 'can be split at the end', async ( { editor, page } ) => { + await editor.insertBlock( { name: 'core/quote' } ); + await page.keyboard.type( '1' ); + await page.keyboard.press( 'Enter' ); + await page.keyboard.press( 'Enter' ); + // Expect empty paragraph outside quote block. + expect( await editor.getEditedPostContent() ).toBe( `
-

Quote content

+

1

+
+ + + +

+` + ); + await page.keyboard.press( 'Backspace' ); + await page.keyboard.type( '2' ); + // Expect the paragraph to be merged into the quote block. + expect( await editor.getEditedPostContent() ).toBe( + ` +
+

1

+ + + +

2

+
+` + ); + } ); + + test( 'can be unwrapped on Backspace', async ( { editor, page } ) => { + await editor.insertBlock( { name: 'core/quote' } ); + expect( await editor.getEditedPostContent() ).toBe( + ` +
+

+` + ); + await page.keyboard.press( 'Backspace' ); + expect( await editor.getEditedPostContent() ).toBe( '' ); + } ); + + test( 'can be unwrapped with content on Backspace', async ( { + editor, + page, + pageUtils, + } ) => { + await editor.insertBlock( { name: 'core/quote' } ); + await page.keyboard.type( '1' ); + await page.keyboard.press( 'ArrowRight' ); + await page.keyboard.type( '2' ); + expect( await editor.getEditedPostContent() ).toBe( + ` +
+

1

+2
+` + ); + // Move the cursor to the start of the first paragraph of the quoted block. + await pageUtils.pressKeyTimes( 'ArrowLeft', 4 ); + await page.keyboard.press( 'Backspace' ); + expect( await editor.getEditedPostContent() ).toBe( + ` +

1

+ + + +
2
` ); } ); diff --git a/test/e2e/specs/editor/various/__snapshots__/Content-only-lock-should-be-able-to-edit-the-content-of-blocks-with-content-only-lock-1-chromium.txt b/test/e2e/specs/editor/various/__snapshots__/Content-only-lock-should-be-able-to-edit-the-content-of-blocks-with-content-only-lock-1-chromium.txt new file mode 100644 index 0000000000000..18f0839c8dc89 --- /dev/null +++ b/test/e2e/specs/editor/various/__snapshots__/Content-only-lock-should-be-able-to-edit-the-content-of-blocks-with-content-only-lock-1-chromium.txt @@ -0,0 +1,5 @@ + +
+

Hello World

+
+ \ No newline at end of file diff --git a/test/e2e/specs/editor/various/content-only-lock.spec.js b/test/e2e/specs/editor/various/content-only-lock.spec.js new file mode 100644 index 0000000000000..41626ed986b8f --- /dev/null +++ b/test/e2e/specs/editor/various/content-only-lock.spec.js @@ -0,0 +1,31 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +test.describe( 'Content-only lock', () => { + test.beforeEach( async ( { admin } ) => { + await admin.createNewPost(); + } ); + + test( 'should be able to edit the content of blocks with content-only lock', async ( { + editor, + page, + pageUtils, + } ) => { + // Add content only locked block in the code editor + await pageUtils.pressKeyWithModifier( 'secondary', 'M' ); // Emulates CTRL+Shift+Alt + M => toggle code editor + await page.click( '.editor-post-text-editor' ); + await page.keyboard + .type( ` +
+

Hello

+
+ ` ); + await pageUtils.pressKeyWithModifier( 'secondary', 'M' ); + + await page.click( 'role=document[name="Paragraph block"i]' ); + await page.keyboard.type( ' World' ); + expect( await editor.getEditedPostContent() ).toMatchSnapshot(); + } ); +} ); diff --git a/test/e2e/specs/site-editor/push-to-global-styles.spec.js b/test/e2e/specs/site-editor/push-to-global-styles.spec.js new file mode 100644 index 0000000000000..ce854f476e4f9 --- /dev/null +++ b/test/e2e/specs/site-editor/push-to-global-styles.spec.js @@ -0,0 +1,109 @@ +/** + * WordPress dependencies + */ +const { + test, + expect, + Editor, +} = require( '@wordpress/e2e-test-utils-playwright' ); + +test.use( { + editor: async ( { page }, use ) => { + await use( new Editor( { page, hasIframe: true } ) ); + }, +} ); + +test.describe( 'Push to Global Styles button', () => { + test.beforeAll( async ( { requestUtils } ) => { + await Promise.all( [ + requestUtils.activateTheme( 'emptytheme' ), + requestUtils.deleteAllTemplates( 'wp_template' ), + requestUtils.deleteAllTemplates( 'wp_template_part' ), + ] ); + } ); + + test.afterAll( async ( { requestUtils } ) => { + await requestUtils.activateTheme( 'twentytwentyone' ); + } ); + + test.beforeEach( async ( { admin, siteEditor } ) => { + await admin.visitSiteEditor(); + await siteEditor.enterEditMode(); + } ); + + test( 'should apply Heading block styles to all Heading blocks', async ( { + page, + editor, + } ) => { + // Add a Heading block. + await editor.insertBlock( { name: 'core/heading' } ); + await page.keyboard.type( 'A heading' ); + + // Navigate to Styles -> Blocks -> Heading -> Typography + await page.getByRole( 'button', { name: 'Styles' } ).click(); + await page.getByRole( 'button', { name: 'Blocks styles' } ).click(); + await page + .getByRole( 'button', { name: 'Heading block styles' } ) + .click(); + await page.getByRole( 'button', { name: 'Typography styles' } ).click(); + + // Headings should not have uppercase + await expect( + page.getByRole( 'button', { name: 'Uppercase' } ) + ).toHaveAttribute( 'aria-pressed', 'false' ); + + // Go to block settings and open the Advanced panel + await page.getByRole( 'button', { name: 'Settings' } ).click(); + await page.getByRole( 'button', { name: 'Advanced' } ).click(); + + // Push button should be disabled + await expect( + page.getByRole( 'button', { + name: 'Push changes to Global Styles', + } ) + ).toBeDisabled(); + + // Make the Heading block uppercase + await page.getByRole( 'button', { name: 'Uppercase' } ).click(); + + // Push button should now be enabled + await expect( + page.getByRole( 'button', { + name: 'Push changes to Global Styles', + } ) + ).toBeEnabled(); + + // Press the Push button + await page + .getByRole( 'button', { name: 'Push changes to Global Styles' } ) + .click(); + + // Snackbar notification should appear + await expect( + page.getByRole( 'button', { + name: 'Dismiss this notice', + text: 'Pushed styles to all Heading blocks.', + } ) + ).toBeVisible(); + + // Push button should be disabled again + await expect( + page.getByRole( 'button', { + name: 'Push changes to Global Styles', + } ) + ).toBeDisabled(); + + // Navigate again to Styles -> Blocks -> Heading -> Typography + await page.getByRole( 'button', { name: 'Styles' } ).click(); + await page.getByRole( 'button', { name: 'Blocks styles' } ).click(); + await page + .getByRole( 'button', { name: 'Heading block styles' } ) + .click(); + await page.getByRole( 'button', { name: 'Typography styles' } ).click(); + + // Headings should now have uppercase + await expect( + page.getByRole( 'button', { name: 'Uppercase' } ) + ).toHaveAttribute( 'aria-pressed', 'true' ); + } ); +} ); diff --git a/test/integration/fixtures/blocks/core__query__deprecated-2-with-colors.html b/test/integration/fixtures/blocks/core__query__deprecated-2-with-colors.html new file mode 100644 index 0000000000000..e85e61b5d7b14 --- /dev/null +++ b/test/integration/fixtures/blocks/core__query__deprecated-2-with-colors.html @@ -0,0 +1,6 @@ + + + diff --git a/test/integration/fixtures/blocks/core__query__deprecated-2-with-colors.json b/test/integration/fixtures/blocks/core__query__deprecated-2-with-colors.json new file mode 100644 index 0000000000000..82bc41a40fb1b --- /dev/null +++ b/test/integration/fixtures/blocks/core__query__deprecated-2-with-colors.json @@ -0,0 +1,72 @@ +[ + { + "name": "core/query", + "isValid": true, + "attributes": { + "queryId": 19, + "query": { + "perPage": 3, + "pages": 0, + "offset": 0, + "postType": "post", + "order": "desc", + "orderBy": "date", + "author": "", + "search": "", + "exclude": [], + "sticky": "", + "inherit": false, + "taxQuery": { + "category": [ 3, 5 ], + "post_tag": [ 6 ] + } + }, + "tagName": "div", + "displayLayout": { + "type": "list" + } + }, + "innerBlocks": [ + { + "name": "core/group", + "isValid": true, + "attributes": { + "tagName": "div", + "textColor": "pale-cyan-blue", + "style": { + "color": { + "background": "#284d5f" + }, + "elements": { + "link": { + "color": { + "text": "var:preset|color|luminous-vivid-amber" + } + } + } + } + }, + "innerBlocks": [ + { + "name": "core/post-template", + "isValid": true, + "attributes": {}, + "innerBlocks": [ + { + "name": "core/post-title", + "isValid": true, + "attributes": { + "level": 2, + "isLink": false, + "rel": "", + "linkTarget": "_self" + }, + "innerBlocks": [] + } + ] + } + ] + } + ] + } +] diff --git a/test/integration/fixtures/blocks/core__query__deprecated-2-with-colors.parsed.json b/test/integration/fixtures/blocks/core__query__deprecated-2-with-colors.parsed.json new file mode 100644 index 0000000000000..73e4f9b6c18f4 --- /dev/null +++ b/test/integration/fixtures/blocks/core__query__deprecated-2-with-colors.parsed.json @@ -0,0 +1,59 @@ +[ + { + "blockName": "core/query", + "attrs": { + "queryId": 19, + "query": { + "perPage": 3, + "pages": 0, + "offset": 0, + "postType": "post", + "categoryIds": [ 3, 5 ], + "tagIds": [ 6 ], + "order": "desc", + "orderBy": "date", + "author": "", + "search": "", + "exclude": [], + "sticky": "", + "inherit": false + }, + "style": { + "color": { + "background": "#284d5f" + }, + "elements": { + "link": { + "color": { + "text": "var:preset|color|luminous-vivid-amber" + } + } + } + }, + "textColor": "pale-cyan-blue" + }, + "innerBlocks": [ + { + "blockName": "core/post-template", + "attrs": {}, + "innerBlocks": [ + { + "blockName": "core/post-title", + "attrs": {}, + "innerBlocks": [], + "innerHTML": "", + "innerContent": [] + } + ], + "innerHTML": "\n\n", + "innerContent": [ "\n", null, "\n" ] + } + ], + "innerHTML": "\n
\n
\n", + "innerContent": [ + "\n
", + null, + "\n
\n" + ] + } +] diff --git a/test/integration/fixtures/blocks/core__query__deprecated-2-with-colors.serialized.html b/test/integration/fixtures/blocks/core__query__deprecated-2-with-colors.serialized.html new file mode 100644 index 0000000000000..f86b4f26ecc1d --- /dev/null +++ b/test/integration/fixtures/blocks/core__query__deprecated-2-with-colors.serialized.html @@ -0,0 +1,7 @@ + +
+ +
+ diff --git a/test/integration/fixtures/blocks/core__query__deprecated-3.html b/test/integration/fixtures/blocks/core__query__deprecated-3.html new file mode 100644 index 0000000000000..9da20c78184e2 --- /dev/null +++ b/test/integration/fixtures/blocks/core__query__deprecated-3.html @@ -0,0 +1,23 @@ + + + \ No newline at end of file diff --git a/test/integration/fixtures/blocks/core__query__deprecated-3.json b/test/integration/fixtures/blocks/core__query__deprecated-3.json new file mode 100644 index 0000000000000..bb1478cd676f2 --- /dev/null +++ b/test/integration/fixtures/blocks/core__query__deprecated-3.json @@ -0,0 +1,133 @@ +[ + { + "name": "core/query", + "isValid": true, + "attributes": { + "queryId": 3, + "query": { + "perPage": 3, + "pages": 0, + "offset": 0, + "postType": "post", + "order": "desc", + "orderBy": "date", + "author": "", + "search": "", + "exclude": [], + "sticky": "", + "inherit": false + }, + "tagName": "div", + "displayLayout": { + "type": "flex", + "columns": 3 + }, + "align": "wide" + }, + "innerBlocks": [ + { + "name": "core/group", + "isValid": true, + "attributes": { + "tagName": "div", + "textColor": "pale-cyan-blue", + "style": { + "color": { + "background": "#284d5f" + }, + "elements": { + "link": { + "color": { + "text": "var:preset|color|luminous-vivid-amber" + } + } + } + } + }, + "innerBlocks": [ + { + "name": "core/post-template", + "isValid": true, + "attributes": { + "fontSize": "large" + }, + "innerBlocks": [ + { + "name": "core/post-title", + "isValid": true, + "attributes": { + "level": 2, + "isLink": false, + "rel": "", + "linkTarget": "_self" + }, + "innerBlocks": [] + }, + { + "name": "core/post-date", + "isValid": true, + "attributes": { + "isLink": false, + "displayType": "date" + }, + "innerBlocks": [] + }, + { + "name": "core/post-excerpt", + "isValid": true, + "attributes": { + "showMoreOnNewLine": true + }, + "innerBlocks": [] + } + ] + }, + { + "name": "core/query-pagination", + "isValid": true, + "attributes": { + "paginationArrow": "none" + }, + "innerBlocks": [ + { + "name": "core/query-pagination-previous", + "isValid": true, + "attributes": {}, + "innerBlocks": [] + }, + { + "name": "core/query-pagination-numbers", + "isValid": true, + "attributes": {}, + "innerBlocks": [] + }, + { + "name": "core/query-pagination-next", + "isValid": true, + "attributes": {}, + "innerBlocks": [] + } + ] + }, + { + "name": "core/query-no-results", + "isValid": true, + "attributes": {}, + "innerBlocks": [ + { + "name": "core/paragraph", + "isValid": true, + "attributes": { + "content": "No results found.", + "dropCap": false, + "placeholder": "Add text or blocks that will display when a query returns no results." + }, + "innerBlocks": [] + } + ] + } + ] + } + ] + } +] diff --git a/test/integration/fixtures/blocks/core__query__deprecated-3.parsed.json b/test/integration/fixtures/blocks/core__query__deprecated-3.parsed.json new file mode 100644 index 0000000000000..a26e573bac314 --- /dev/null +++ b/test/integration/fixtures/blocks/core__query__deprecated-3.parsed.json @@ -0,0 +1,128 @@ +[ + { + "blockName": "core/query", + "attrs": { + "queryId": 3, + "query": { + "perPage": 3, + "pages": 0, + "offset": 0, + "postType": "post", + "order": "desc", + "orderBy": "date", + "author": "", + "search": "", + "exclude": [], + "sticky": "", + "inherit": false + }, + "displayLayout": { + "type": "flex", + "columns": 3 + }, + "align": "wide", + "style": { + "color": { + "background": "#284d5f" + }, + "elements": { + "link": { + "color": { + "text": "var:preset|color|luminous-vivid-amber" + } + } + } + }, + "textColor": "pale-cyan-blue" + }, + "innerBlocks": [ + { + "blockName": "core/post-template", + "attrs": { + "fontSize": "large" + }, + "innerBlocks": [ + { + "blockName": "core/post-title", + "attrs": {}, + "innerBlocks": [], + "innerHTML": "", + "innerContent": [] + }, + { + "blockName": "core/post-date", + "attrs": {}, + "innerBlocks": [], + "innerHTML": "", + "innerContent": [] + }, + { + "blockName": "core/post-excerpt", + "attrs": {}, + "innerBlocks": [], + "innerHTML": "", + "innerContent": [] + } + ], + "innerHTML": "\n\n\n\n\n\n", + "innerContent": [ "\n", null, "\n\n", null, "\n\n", null, "\n" ] + }, + { + "blockName": "core/query-pagination", + "attrs": {}, + "innerBlocks": [ + { + "blockName": "core/query-pagination-previous", + "attrs": {}, + "innerBlocks": [], + "innerHTML": "", + "innerContent": [] + }, + { + "blockName": "core/query-pagination-numbers", + "attrs": {}, + "innerBlocks": [], + "innerHTML": "", + "innerContent": [] + }, + { + "blockName": "core/query-pagination-next", + "attrs": {}, + "innerBlocks": [], + "innerHTML": "", + "innerContent": [] + } + ], + "innerHTML": "\n\n\n\n\n\n", + "innerContent": [ "\n", null, "\n\n", null, "\n\n", null, "\n" ] + }, + { + "blockName": "core/query-no-results", + "attrs": {}, + "innerBlocks": [ + { + "blockName": "core/paragraph", + "attrs": { + "placeholder": "Add text or blocks that will display when a query returns no results." + }, + "innerBlocks": [], + "innerHTML": "\n

No results found.

\n", + "innerContent": [ "\n

No results found.

\n" ] + } + ], + "innerHTML": "\n\n", + "innerContent": [ "\n", null, "\n" ] + } + ], + "innerHTML": "\n
\n\n\n\n
\n", + "innerContent": [ + "\n
", + null, + "\n\n", + null, + "\n\n", + null, + "
\n" + ] + } +] diff --git a/test/integration/fixtures/blocks/core__query__deprecated-3.serialized.html b/test/integration/fixtures/blocks/core__query__deprecated-3.serialized.html new file mode 100644 index 0000000000000..edbf5b1a0557b --- /dev/null +++ b/test/integration/fixtures/blocks/core__query__deprecated-3.serialized.html @@ -0,0 +1,25 @@ + +
+ +
+