From 0944f75ee33fe77e58c82e31d85c2759c715081e Mon Sep 17 00:00:00 2001 From: Dave Smith Date: Wed, 14 Dec 2022 17:23:43 +0000 Subject: [PATCH 01/51] Nav offcanvas - handle non-direct insert block inserter (#46503) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Pass blocks from block inserter to custom appender callback * Allow only blocks which support Link UI to trigger it’s rendering on insertion * Allow Link UI for all supporting blocks * Extract clearly named function to improve comprehension * Allow for programmatic disabling of selecting block on insertion via Quick Inserter * Fix stray false * Provide correct default to ensure behaviour is backwards compatible * Always pass selectBlockOnInsert --- .../inserter/hooks/use-insertion-point.js | 5 +- .../src/components/inserter/index.js | 17 ++++++- .../src/components/inserter/quick-inserter.js | 3 ++ .../src/components/inserter/search-results.js | 2 + .../components/off-canvas-editor/appender.js | 50 +++++++++++++------ 5 files changed, 58 insertions(+), 19 deletions(-) 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 d2bbaa909d0a29..52c7f9ba23f83e 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 59c04e24d2bbb4..258faff2b826ab 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/quick-inserter.js b/packages/block-editor/src/components/inserter/quick-inserter.js index 39a5ac5c75b849..540b51a4757e0d 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 dfd7a3d73312d5..f55e49bd1cc80f 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 646700143cf511..0fb19df664f016 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 } />
From 04d69dde75a6c6a305b5b89c4adf2621f7b9c600 Mon Sep 17 00:00:00 2001 From: Ben Dwyer Date: Wed, 14 Dec 2022 17:30:42 +0000 Subject: [PATCH 02/51] Page List: Update the icon to demonstrate that the pages are automatically updated (#46438) * Page List: Update the icon to demonstrate that the pages are automatically updated * Allow a context to be passed to the block icon --- .../block-editor/src/components/block-icon/index.js | 6 ++++-- .../off-canvas-editor/block-select-button.js | 6 +++++- packages/block-library/src/page-list/index.js | 10 ++++++++-- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/packages/block-editor/src/components/block-icon/index.js b/packages/block-editor/src/components/block-icon/index.js index ad3484c9e4a049..58d7b3a04adb9e 100644 --- a/packages/block-editor/src/components/block-icon/index.js +++ b/packages/block-editor/src/components/block-icon/index.js @@ -10,14 +10,16 @@ import { Icon } from '@wordpress/components'; import { blockDefault } from '@wordpress/icons'; import { memo } from '@wordpress/element'; -function BlockIcon( { icon, showColors = false, className } ) { +function BlockIcon( { icon, showColors = false, className, context } ) { if ( icon?.src === 'block-default' ) { icon = { src: blockDefault, }; } - const renderedIcon = ; + const renderedIcon = ( + + ); const style = showColors ? { backgroundColor: icon && icon.background, 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 9477eb2cda40c0..a3cb64e9298dd4 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 } > - + { + if ( context === 'list-view' ) { + return update; + } + + return pages; + }, example: {}, edit, }; From 2437a110c99c6e6e08794600300064c282004d67 Mon Sep 17 00:00:00 2001 From: Gutenberg Repository Automation Date: Wed, 14 Dec 2022 17:46:08 +0000 Subject: [PATCH 03/51] Update Changelog for 14.8.0-rc.1 --- changelog.txt | 283 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 283 insertions(+) diff --git a/changelog.txt b/changelog.txt index 2d52f25c1bf8a6..8774c72580adff 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,288 @@ == 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 From eadd4b5af5abc800db3f6e8ba6959b38221ecd6d Mon Sep 17 00:00:00 2001 From: Ryan Welcher Date: Wed, 14 Dec 2022 13:58:14 -0500 Subject: [PATCH 04/51] Clarifying the npm package release process for the RC release. (#46555) --- docs/contributors/code/release.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/contributors/code/release.md b/docs/contributors/code/release.md index f463cb319f6657..492813376b2c9d 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). From 271f650c05795ab81cc73b2617ba53485ac57de9 Mon Sep 17 00:00:00 2001 From: Marcelo Serpa <81248+fullofcaffeine@users.noreply.github.com> Date: Wed, 14 Dec 2022 16:23:05 -0600 Subject: [PATCH 05/51] [`customize-widgets/utils/widgetToBlock`] Initialize a widget's `raw_content.content` to an empty string if it's `undefined` (#46487) * Initialize a widget's raw_content.content to an empty string if it's undefined * Do not mutate the instance object's raw_instance property * Add comment --- packages/customize-widgets/src/utils.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/customize-widgets/src/utils.js b/packages/customize-widgets/src/utils.js index f2f254b5ae6af6..8cf2d5990423ca 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, From b856a3fc6b72b3c48f35608c68b83d0953959f47 Mon Sep 17 00:00:00 2001 From: Artemio Morales Date: Wed, 14 Dec 2022 18:29:02 -0500 Subject: [PATCH 06/51] Add rough animation to navigation and links (#46342) * Add rough animation to navigation and links * Add support for configuring animation in block settings; revise animation implementation * Fix animation direction; do code cleanup * Add framer dependency and disable import restriction * Fix linter errors * Limit scope to fix tests * Add wrapper for animation logic, update useSelect dependencies * Fix dependency declaration that was causing tests to break --- ...-rest-block-editor-settings-controller.php | 6 ++ .../src/components/block-inspector/index.js | 83 +++++++++++++++++-- .../src/navigation-link/index.php | 32 +++++++ .../src/navigation-submenu/index.php | 32 +++++++ .../block-library/src/navigation/index.php | 32 +++++++ 5 files changed, 180 insertions(+), 5 deletions(-) diff --git a/lib/experimental/class-wp-rest-block-editor-settings-controller.php b/lib/experimental/class-wp-rest-block-editor-settings-controller.php index d95fb497d3a0a3..7031a14e89ccac 100644 --- a/lib/experimental/class-wp-rest-block-editor-settings-controller.php +++ b/lib/experimental/class-wp-rest-block-editor-settings-controller.php @@ -174,6 +174,12 @@ public function get_item_schema() { 'context' => 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/packages/block-editor/src/components/block-inspector/index.js b/packages/block-editor/src/components/block-inspector/index.js index d676bf5dac6a5b..2f3b69131a73e6 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,22 @@ 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 +248,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-library/src/navigation-link/index.php b/packages/block-library/src/navigation-link/index.php index 8853d49f6e2cf0..36bfbf5d1fc0ae 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-submenu/index.php b/packages/block-library/src/navigation-submenu/index.php index 99c86493d5b31a..c1c8006039f555 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/index.php b/packages/block-library/src/navigation/index.php index 030cab4501eed4..971a5f500e7741 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' ); From 489d48a119477e44022e9a3939332bdcc6cc94aa Mon Sep 17 00:00:00 2001 From: Andrew Serong <14988353+andrewserong@users.noreply.github.com> Date: Thu, 15 Dec 2022 15:02:03 +1100 Subject: [PATCH 07/51] Global Styles REST API endpoint: check custom CSS is included before attempting to validate (#46561) * Global Styles REST API endpoint: check custom CSS is included before attempting to validate * Fix linting issue --- ...ass-gutenberg-rest-global-styles-controller-6-2.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) 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 index 9f762dd961d058..684786ef22d762 100644 --- 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 @@ -151,10 +151,12 @@ protected function prepare_item_for_database( $request ) { if ( isset( $request['styles'] ) || isset( $request['settings'] ) ) { $config = array(); if ( isset( $request['styles'] ) ) { - $config['styles'] = $request['styles']; - $validate_custom_css = $this->validate_custom_css( $request['styles']['css'] ); - if ( is_wp_error( $validate_custom_css ) ) { - return $validate_custom_css; + $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']; From 1e6538ba9d6f07a4c7a2fc0ef63ac0ebe932df80 Mon Sep 17 00:00:00 2001 From: George Mamadashvili Date: Thu, 15 Dec 2022 09:11:05 +0400 Subject: [PATCH 08/51] Template Parts: Add an option to import widgets from the sidebars (#45509) * Template Part: Introduce an option to import widgets * Only display active sidebars with widgets as options * Use the sidebar name as the new template part title * Support legacy widgets * Prefix imported template parts to avoid name collision * Add missing docblock * Update labels * Move settings into the advanced inspector controls panel * Update API response for sidebars and mark them as 'inactive' * A minor design adjustments * Fix the rendering order bug * Transform legacy widgets before importing * Avoid hardcoding names of the blocks with wildcard transformations * Skip 'wp_inactive_widgets' widget area * Use 'HStack' * Allow overriding Legacy Widget block settings during registration * Override only supports settings * Improve spacing * Add the legacy widget to the post editor Co-authored-by: Robert Anderson --- lib/compat/wordpress-6.2/rest-api.php | 15 ++ lib/compat/wordpress-6.2/theme.php | 23 +++ lib/compat/wordpress-6.2/widgets.php | 32 ++++ lib/load.php | 2 + package-lock.json | 2 + .../template-part/edit/advanced-controls.js | 12 ++ .../src/template-part/edit/import-controls.js | 180 ++++++++++++++++++ .../src/template-part/edit/index.js | 1 + .../template-part/edit/utils/transformers.js | 37 ++++ packages/edit-post/package.json | 1 + packages/edit-post/src/index.js | 2 + packages/edit-site/package.json | 1 + packages/edit-site/src/index.js | 2 + packages/widgets/src/index.js | 14 +- 14 files changed, 322 insertions(+), 2 deletions(-) create mode 100644 lib/compat/wordpress-6.2/theme.php create mode 100644 lib/compat/wordpress-6.2/widgets.php create mode 100644 packages/block-library/src/template-part/edit/import-controls.js create mode 100644 packages/block-library/src/template-part/edit/utils/transformers.js diff --git a/lib/compat/wordpress-6.2/rest-api.php b/lib/compat/wordpress-6.2/rest-api.php index 4900a622b6c01b..12f7afda3b4d5d 100644 --- a/lib/compat/wordpress-6.2/rest-api.php +++ b/lib/compat/wordpress-6.2/rest-api.php @@ -101,3 +101,18 @@ function gutenberg_register_global_styles_endpoints() { $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/theme.php b/lib/compat/wordpress-6.2/theme.php new file mode 100644 index 00000000000000..79d55206449472 --- /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 00000000000000..19591ae64607e3 --- /dev/null +++ b/lib/compat/wordpress-6.2/widgets.php @@ -0,0 +1,32 @@ + 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 00000000000000..1512cd936f02cb --- /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 83b1a8d450b6ca..502bd3d00feef6 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/transformers.js b/packages/block-library/src/template-part/edit/utils/transformers.js new file mode 100644 index 00000000000000..fdef84d785b909 --- /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/edit-post/package.json b/packages/edit-post/package.json index 3414e958344d23..7967b0d63b1d62 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 5b7c4e01ead7b1..2ee4dcdab17049 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/package.json b/packages/edit-site/package.json index 6f42f00b2c625e..a89f52e5dddda5 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/index.js b/packages/edit-site/src/index.js index 4b619f7aac9a97..d2cbb2f4aa04d2 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/widgets/src/index.js b/packages/widgets/src/index.js index 6c88d65556d8b0..e55b16ff12b35d 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, + }, + } + ); } /** From d8d2d5d03132b67a65b7e78da37a949cada93cee Mon Sep 17 00:00:00 2001 From: George Mamadashvili Date: Thu, 15 Dec 2022 10:35:48 +0400 Subject: [PATCH 09/51] Change SpacingSizesControl ARIA from region to group (#46530) * Change SpacingSizesControl ARIA from region to group * Add changelog entry * Use the correct changelog file --- packages/block-editor/CHANGELOG.md | 6 +++++- .../src/components/spacing-sizes-control/index.js | 1 - 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/block-editor/CHANGELOG.md b/packages/block-editor/CHANGELOG.md index 6abc73d7073b60..21666d999c1d15 100644 --- a/packages/block-editor/CHANGELOG.md +++ b/packages/block-editor/CHANGELOG.md @@ -8,7 +8,11 @@ ### Enhancements -- `URLInput`: the `renderSuggestions` callback prop now receives `currentInputValue` as a new parameter ([45806](https://github.com/WordPress/gutenberg/pull/45806)). +- `URLInput`: the `renderSuggestions` callback prop now receives `currentInputValue` as a new parameter ([45806](https://github.com/WordPress/gutenberg/pull/45806)). + +### Bug Fix + +- `SpacingSizesControl`: Change ARIA role from `region` to `group` to avoid unwanted ARIA landmark regions ([#46530](https://github.com/WordPress/gutenberg/pull/46530)). ## 10.5.0 (2022-11-16) 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 fb4ce2176b759d..4ec1285db52bb9 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 (
Date: Thu, 15 Dec 2022 14:59:03 +0800 Subject: [PATCH 10/51] Update copy of add submenu item option (#46564) --- packages/block-editor/src/components/off-canvas-editor/block.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/block-editor/src/components/off-canvas-editor/block.js b/packages/block-editor/src/components/off-canvas-editor/block.js index 49ce5788dfafe0..f07d22bcea3fe0 100644 --- a/packages/block-editor/src/components/off-canvas-editor/block.js +++ b/packages/block-editor/src/components/off-canvas-editor/block.js @@ -386,7 +386,7 @@ function ListViewBlock( { onClose(); } } > - { __( 'Add a submenu item' ) } + { __( 'Add submenu item' ) } ) } From 547542478d3e1d8ca4bc3599c1a437ca27094d2a Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Thu, 15 Dec 2022 15:08:22 +0800 Subject: [PATCH 11/51] Fix Off Canvas Editor add submenu item option (#46562) * Fix Off Canvas Editor add submenu item option * Fix comment --- .../src/components/off-canvas-editor/block.js | 55 ++++++++++++------- 1 file changed, 34 insertions(+), 21 deletions(-) diff --git a/packages/block-editor/src/components/off-canvas-editor/block.js b/packages/block-editor/src/components/off-canvas-editor/block.js index f07d22bcea3fe0..4ad64587796bd7 100644 --- a/packages/block-editor/src/components/off-canvas-editor/block.js +++ b/packages/block-editor/src/components/off-canvas-editor/block.js @@ -42,7 +42,7 @@ import useBlockDisplayInformation from '../use-block-display-information'; import { useBlockLock } from '../block-lock'; function ListViewBlock( { - block, + block: { clientId }, isDragged, isSelected, isBranchSelected, @@ -60,7 +60,6 @@ function ListViewBlock( { } ) { const cellRef = useRef( null ); const [ isHovered, setIsHovered ] = useState( false ); - const { clientId, attributes } = block; const { isLocked, isContentLocked } = useBlockLock( clientId ); const forceSelectionContentLock = useSelect( @@ -87,12 +86,12 @@ function ListViewBlock( { ( isSelected && selectedClientIds[ selectedClientIds.length - 1 ] === clientId ); - const { replaceBlock, toggleBlockHighlight } = + const { insertBlock, replaceBlock, toggleBlockHighlight } = useDispatch( blockEditorStore ); const blockInformation = useBlockDisplayInformation( clientId ); - const blockName = useSelect( - ( select ) => select( blockEditorStore ).getBlockName( clientId ), + const block = useSelect( + ( select ) => select( blockEditorStore ).getBlock( clientId ), [ clientId ] ); @@ -100,7 +99,7 @@ function ListViewBlock( { // 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, + block.name, '__experimentalToolbar', true ); @@ -145,7 +144,7 @@ function ListViewBlock( { const { isTreeGridMounted, expand, collapse } = useListViewContext(); - const isEditable = blockName !== 'core/page-list-item'; + const isEditable = block.name !== 'core/page-list-item'; const hasSiblings = siblingBlockCount > 0; const hasRenderedMovers = showBlockMovers && hasSiblings; const moverCellClassName = classnames( @@ -369,20 +368,34 @@ function ListViewBlock( { const newLink = createBlock( 'core/navigation-link' ); - const newSubmenu = createBlock( - 'core/navigation-submenu', - attributes, - block.innerBlocks - ? [ - ...block.innerBlocks, - newLink, - ] - : [ newLink ] - ); - replaceBlock( - clientId, - newSubmenu - ); + if ( + block.name === + 'core/navigation-submenu' + ) { + const updateSelectionOnInsert = false; + insertBlock( + newLink, + block.innerBlocks.length, + clientId, + updateSelectionOnInsert + ); + } else { + // Convert to a submenu if the block currently isn't one. + const newSubmenu = createBlock( + 'core/navigation-submenu', + block.attributes, + block.innerBlocks + ? [ + ...block.innerBlocks, + newLink, + ] + : [ newLink ] + ); + replaceBlock( + clientId, + newSubmenu + ); + } onClose(); } } > From 3331ee1de8147d613ee45fdb1d20d303c9285b6b Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Thu, 15 Dec 2022 15:20:35 +0800 Subject: [PATCH 12/51] Try controlled page list block (#46416) --- packages/block-library/src/page-list/edit.js | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/packages/block-library/src/page-list/edit.js b/packages/block-library/src/page-list/edit.js index b3d2db31d5e04a..7372191723f466 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 ) => { From 9615ed0288e9d32c39f515396b7dd6df337d37a5 Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Thu, 15 Dec 2022 15:36:25 +0800 Subject: [PATCH 13/51] Style Engine: Add support for text columns (column-count) (#46566) --- packages/style-engine/class-wp-style-engine.php | 6 ++++++ .../style-engine/src/styles/typography/index.ts | 13 +++++++++++++ packages/style-engine/src/test/index.js | 9 ++++++++- packages/style-engine/src/types.ts | 1 + phpunit/style-engine/style-engine-test.php | 4 +++- 5 files changed, 31 insertions(+), 2 deletions(-) diff --git a/packages/style-engine/class-wp-style-engine.php b/packages/style-engine/class-wp-style-engine.php index f5636c150c3eeb..0fd0aeac60e7c4 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 48effa0b2b8a95..75e02c96430512 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 a94bd8a4256810..fac55b4000e58f 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 0063cd56621c6c..23d3e38cc43c22 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/phpunit/style-engine/style-engine-test.php b/phpunit/style-engine/style-engine-test.php index 66d8dd62865272..0588f10ef4fce7 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', From a58c37077e5f24ab5c55f750517266c92dbd22f0 Mon Sep 17 00:00:00 2001 From: Ella <4710635+ellatrix@users.noreply.github.com> Date: Thu, 15 Dec 2022 09:55:35 +0200 Subject: [PATCH 14/51] Paste: reuse file transforms for file pasting (#45891) * Paste: reuse file transforms for file pasting * Make sure files is defined * paste multiple file types * fix for gallery block * Make sure gallery block settings sync Co-authored-by: ntsekouras --- .../src/components/copy-handler/index.js | 60 ++++++++++++++++--- .../rich-text/file-paste-handler.js | 13 ---- .../components/rich-text/use-paste-handler.js | 47 ++++++++++----- packages/block-editor/src/utils/pasting.js | 12 +--- packages/block-library/src/gallery/edit.js | 18 +++++- packages/block-library/src/gallery/gallery.js | 16 +---- 6 files changed, 108 insertions(+), 58 deletions(-) delete mode 100644 packages/block-editor/src/components/rich-text/file-paste-handler.js diff --git a/packages/block-editor/src/components/copy-handler/index.js b/packages/block-editor/src/components/copy-handler/index.js index 3e5a50a8fe49ea..3881ff06f2bf30 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/rich-text/file-paste-handler.js b/packages/block-editor/src/components/rich-text/file-paste-handler.js deleted file mode 100644 index 2aae5984389e68..00000000000000 --- 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 5e4f4260f5001f..67c932aceddcc1 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/utils/pasting.js b/packages/block-editor/src/utils/pasting.js index 366b79a3294229..e962e11050a1d9 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/gallery/edit.js b/packages/block-library/src/gallery/edit.js index c8d6731198c7c6..893167e6690f3c 100644 --- a/packages/block-library/src/gallery/edit.js +++ b/packages/block-library/src/gallery/edit.js @@ -20,6 +20,7 @@ import { MediaPlaceholder, InspectorControls, useBlockProps, + useInnerBlocksProps, BlockControls, MediaReplaceFlow, } from '@wordpress/block-editor'; @@ -63,6 +64,7 @@ const linkOptions = [ }, ]; const ALLOWED_MEDIA_TYPES = [ 'image' ]; +const allowedBlocks = [ 'core/image' ]; const PLACEHOLDER_TEXT = Platform.isNative ? __( 'ADD MEDIA' ) @@ -483,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'; @@ -579,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 e6176cc8a7256c..957a141d51c6b7 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 } From 897d3b3ab9f9edb57bea57972ea830b483309448 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Thu, 15 Dec 2022 10:04:32 +0100 Subject: [PATCH 15/51] Add a performance metric to measure typing within containers (#46529) --- bin/plugin/commands/performance.js | 16 ++++ .../assets/small-post-with-containers.html | 77 ++++++++++++++++ .../e2e-tests/config/performance-reporter.js | 28 ++++-- .../specs/performance/post-editor.test.js | 91 +++++++++++++++---- .../specs/performance/site-editor.test.js | 1 + 5 files changed, 189 insertions(+), 24 deletions(-) create mode 100644 packages/e2e-tests/assets/small-post-with-containers.html diff --git a/bin/plugin/commands/performance.js b/bin/plugin/commands/performance.js index 31a537f373731d..81aa39820c8b5a 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/packages/e2e-tests/assets/small-post-with-containers.html b/packages/e2e-tests/assets/small-post-with-containers.html new file mode 100644 index 00000000000000..f5d32011602dcd --- /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 549855bf297135..e9928008dd6e3b 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/performance/post-editor.test.js b/packages/e2e-tests/specs/performance/post-editor.test.js index 933b4973fb055e..63fa92514728ec 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 eaa3323b7b16c8..e3a2ef86c69328 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: [], From abf426bd83a4ab1469587bd5dabfff6861414fb0 Mon Sep 17 00:00:00 2001 From: Marin Atanasov <8436925+tyxla@users.noreply.github.com> Date: Thu, 15 Dec 2022 11:20:10 +0200 Subject: [PATCH 16/51] Lodash: Refactor components away from `_.find()` (#46537) * Lodash: Refactor components away from _.find() * Add CHANGELOG entry --- packages/components/CHANGELOG.md | 4 +++ packages/components/src/autocomplete/index.js | 4 +-- .../mobile/bottom-sheet/picker-cell.native.js | 7 +--- .../global-styles-context/utils.native.js | 33 ++++++++++--------- packages/components/src/tab-panel/index.tsx | 3 +- 5 files changed, 24 insertions(+), 27 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 7ed9fc6779bbae..f459061f109e19 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -45,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 45f5415e55de00..13c27d49b81b15 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/mobile/bottom-sheet/picker-cell.native.js b/packages/components/src/mobile/bottom-sheet/picker-cell.native.js index 42c59cd025adf0..2dbbdd08d4609e 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 7d985a9cccb6f6..38536ec3f107f9 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/tab-panel/index.tsx b/packages/components/src/tab-panel/index.tsx index fce688089a55c0..2504f7f214fa62 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( () => { From 43f2ef4c542faa6be9f80c58cc0628ee4578a579 Mon Sep 17 00:00:00 2001 From: Dave Smith Date: Thu, 15 Dec 2022 09:31:25 +0000 Subject: [PATCH 17/51] Fix accessing potentially undefined block editor setting __experimentalBlockInspectorAnimation --- packages/block-editor/src/components/block-inspector/index.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/block-editor/src/components/block-inspector/index.js b/packages/block-editor/src/components/block-inspector/index.js index 2f3b69131a73e6..48d5d982e75089 100644 --- a/packages/block-editor/src/components/block-inspector/index.js +++ b/packages/block-editor/src/components/block-inspector/index.js @@ -181,7 +181,9 @@ const BlockInspector = ( { showNoBlockSelectedMessage = true } ) => { const globalBlockInspectorAnimationSettings = select( blockEditorStore ).getSettings() .__experimentalBlockInspectorAnimation; - return globalBlockInspectorAnimationSettings[ blockType.name ]; + return globalBlockInspectorAnimationSettings?.[ + blockType.name + ]; } return null; }, From c89dd8bca282386401d458bcfe808d555e143617 Mon Sep 17 00:00:00 2001 From: Ben Dwyer Date: Thu, 15 Dec 2022 10:00:21 +0000 Subject: [PATCH 18/51] Update the update icon (#46575) Co-authored-by: Ben Dwyer --- packages/icons/src/library/update.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/icons/src/library/update.js b/packages/icons/src/library/update.js index d751fb970ea551..09a291c7e97566 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 = ( - - + + ); From ece606040387b85069419801cb1b99cf851058b2 Mon Sep 17 00:00:00 2001 From: Ella <4710635+ellatrix@users.noreply.github.com> Date: Thu, 15 Dec 2022 12:43:42 +0200 Subject: [PATCH 19/51] Block editor: iframe/writing flow: change tab index to 0 (#46323) --- packages/block-editor/src/components/writing-flow/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/block-editor/src/components/writing-flow/index.js b/packages/block-editor/src/components/writing-flow/index.js index b28f065c559c5f..e3100dc3464c65 100644 --- a/packages/block-editor/src/components/writing-flow/index.js +++ b/packages/block-editor/src/components/writing-flow/index.js @@ -44,7 +44,7 @@ export function useWritingFlow() { useArrowNav(), useRefEffect( ( node ) => { - node.tabIndex = -1; + node.tabIndex = 0; node.contentEditable = hasMultiSelection; if ( ! hasMultiSelection ) { From 9c122466100e6e06dd23cca9b6fd744bc97f048a Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Thu, 15 Dec 2022 19:55:01 +0800 Subject: [PATCH 20/51] Page List: Add typography supports (#43316) Co-authored-by: Glen Davies --- docs/reference-guides/core-blocks.md | 2 +- packages/block-library/src/page-list/block.json | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/docs/reference-guides/core-blocks.md b/docs/reference-guides/core-blocks.md index d2d66fc0d12de3..14f800f140aee3 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 diff --git a/packages/block-library/src/page-list/block.json b/packages/block-library/src/page-list/block.json index 2fc6993849d6f7..e8ff316a9fb4f6 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" From aa9b1d665a10f32182797e0d7e7a559362d1ec1c Mon Sep 17 00:00:00 2001 From: Rich Tabor Date: Thu, 15 Dec 2022 08:25:56 -0500 Subject: [PATCH 21/51] Remove clearable from link ColorPanel item (#46507) --- packages/block-editor/src/hooks/color.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/block-editor/src/hooks/color.js b/packages/block-editor/src/hooks/color.js index 593eea881e3107..fee6f39daa8b20 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, }, From 94b4253a0aad603fef897ffdd89df3e19ebba85b Mon Sep 17 00:00:00 2001 From: Aki Hamano <54422211+t-hamano@users.noreply.github.com> Date: Thu, 15 Dec 2022 22:42:01 +0900 Subject: [PATCH 22/51] Migrate quote e2e tests to playwright (#46549) * Migrate qupte e2e tests to playwright * Use inline snapshots * Add `deleteAllPosts` * Use accesible selector * Add an inline comment for `pressKeyTimes` --- .../blocks/__snapshots__/quote.test.js.snap | 123 ------- .../specs/editor/blocks/quote.test.js | 199 ----------- test/e2e/specs/editor/blocks/quote.spec.js | 312 +++++++++++++++++- 3 files changed, 300 insertions(+), 334 deletions(-) delete mode 100644 packages/e2e-tests/specs/editor/blocks/__snapshots__/quote.test.js.snap delete mode 100644 packages/e2e-tests/specs/editor/blocks/quote.test.js 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 9a3924de6d1dab..00000000000000 --- 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 9b0a4bac546dd3..00000000000000 --- 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/test/e2e/specs/editor/blocks/quote.spec.js b/test/e2e/specs/editor/blocks/quote.spec.js index 69d69f0003489f..299d56785d831b 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
` ); } ); From c963b315d714e7c962f3f00d9fe43dc96c3c455e Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Thu, 15 Dec 2022 15:15:14 +0100 Subject: [PATCH 23/51] Fix region navigation in the site editor (#46525) --- packages/base-styles/_z-index.scss | 5 +- .../higher-order/navigate-regions/style.scss | 52 ++++------------ .../edit-site/src/components/editor/index.js | 17 +----- .../edit-site/src/components/layout/index.js | 59 +++++++++++++------ .../edit-site/src/components/list/style.scss | 9 +-- .../components/interface-skeleton/index.js | 8 ++- .../components/interface-skeleton/style.scss | 14 ----- .../src/components/navigable-region/index.js | 4 +- 8 files changed, 66 insertions(+), 102 deletions(-) diff --git a/packages/base-styles/_z-index.scss b/packages/base-styles/_z-index.scss index ec3be48f1b9752..62a1ab3b19d8f0 100644 --- a/packages/base-styles/_z-index.scss +++ b/packages/base-styles/_z-index.scss @@ -142,6 +142,9 @@ $z-layers: ( ".skip-to-selected-block": 100000, ".interface-interface-skeleton__actions": 100000, + // The focus styles of the region navigation containers should be above their content. + ".is-focusing-regions {region} :focus::after": 1000000, + // Show NUX tips above popovers, wp-admin menus, submenus, and sidebar: ".nux-dot-tip": 1000001, @@ -177,8 +180,6 @@ $z-layers: ( // Appear under the topbar. ".customize-widgets__block-toolbar": 7, - - ".is-focusing-regions [role='region']:focus .interface-navigable-region__stacker": -1, ); @function z-index( $key ) { diff --git a/packages/components/src/higher-order/navigate-regions/style.scss b/packages/components/src/higher-order/navigate-regions/style.scss index 807dd0e5dcae5f..6e66c854dd70ee 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/edit-site/src/components/editor/index.js b/packages/edit-site/src/components/editor/index.js index 2f2f6264f2e8d8..fe733f49a19590 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 @@ -64,8 +63,6 @@ export default function Editor() { isInserterOpen, isListViewOpen, isSaveViewOpen, - previousShortcut, - nextShortcut, showIconLabels, } = useSelect( ( select ) => { const { @@ -80,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(); @@ -112,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' @@ -178,6 +166,7 @@ export default function Editor() { ) } - shortcuts={ { - previous: previousShortcut, - next: nextShortcut, - } } labels={ { ...interfaceLabels, secondarySidebar: secondarySidebarLabel, diff --git a/packages/edit-site/src/components/layout/index.js b/packages/edit-site/src/components/layout/index.js index 150f2d9cce882d..e3d2edc217eefd 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 @@ -47,15 +49,28 @@ export default function Layout( { onError } ) { 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(); @@ -108,13 +123,19 @@ export default function Layout( { onError } ) { <> { fullResizer }
@@ -196,7 +217,8 @@ export default function Layout( { onError } ) { { showSidebar && ( - - + ) } diff --git a/packages/edit-site/src/components/list/style.scss b/packages/edit-site/src/components/list/style.scss index b8c8f2be27db1a..c3359b752fc053 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/interface/src/components/interface-skeleton/index.js b/packages/interface/src/components/interface-skeleton/index.js index e8a7a45ede3fa4..fe329a75d43e07 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 } ); } From da03081e5e5fec8345620070b356c0c8f4c0d682 Mon Sep 17 00:00:00 2001 From: Ella <4710635+ellatrix@users.noreply.github.com> Date: Thu, 15 Dec 2022 16:28:43 +0200 Subject: [PATCH 24/51] DOM: fix findNext/Previous tabbable if target is not in findFocusable list (#46580) --- packages/dom/src/tabbable.js | 37 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/packages/dom/src/tabbable.js b/packages/dom/src/tabbable.js index 66ae1ee7d4fd46..25bc81f2154582 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 + ); + } + ); } From 7973d9cc5890b64c78e86cb86e1d52d8df55fc69 Mon Sep 17 00:00:00 2001 From: Ramon Date: Fri, 16 Dec 2022 10:26:16 +1100 Subject: [PATCH 25/51] Fluid typography: add configurable settings for minimum font size to theme.json (#42489) * Reinstating the commit in edeaca7068196f8960453965fe7fa64632d573ef so we can test fluid configuration Update docs * Adding a filter pre-calculations * $fluid_settings was missing * Removing filter Pulling across config changes to JS * Add config slot for minFontSize * Adding config to test theme * Updating args to include `minimumFontSizeLimit` Fixed `typographySettings` check * Updating tests and fixing settings logic Updating schema Changelogs * Ensure that a theme's fluid font size settings are used when calculating custom font sizes and search block font sizes. * Validating incoming viewport widths and min font size against accepted units. * Ensure we check for fluid settings validity in getTypographyClassesAndStyles * Rolling back configurable values to the global minimum font size only. Updating tests. * Remove config assignment from getTypographyClassesAndStyles and updated tests. * Adding frontend test for unsupported units in min font size config * simplifying condition so that it's readable in screen widths < 100000px --- .../theme-json-reference/theme-json-living.md | 2 +- lib/block-supports/typography.php | 11 ++- packages/block-editor/CHANGELOG.md | 1 + packages/block-editor/README.md | 3 +- .../src/components/font-sizes/fluid-utils.js | 8 +- packages/block-editor/src/hooks/font-size.js | 17 +++- .../src/hooks/test/use-typography-props.js | 26 +++++ .../src/hooks/use-typography-props.js | 22 +++-- packages/block-library/src/search/edit.js | 4 +- packages/edit-site/CHANGELOG.md | 6 +- .../global-styles/test/typography-utils.js | 95 ++++++++++++++----- .../global-styles/typography-utils.js | 28 +++++- phpunit/block-supports/typography-test.php | 49 ++++++---- .../style.css | 8 ++ .../theme.json | 11 +++ schemas/json/theme.json | 19 +++- 16 files changed, 243 insertions(+), 67 deletions(-) create mode 100644 phpunit/data/themedir1/block-theme-child-with-fluid-typography-config/style.css create mode 100644 phpunit/data/themedir1/block-theme-child-with-fluid-typography-config/theme.json 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 a694cf63e909fa..9b074edace6ae5 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/lib/block-supports/typography.php b/lib/block-supports/typography.php index 01d223b84281eb..809fba1a6e7aca 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/packages/block-editor/CHANGELOG.md b/packages/block-editor/CHANGELOG.md index 21666d999c1d15..34cd726c55fdf5 100644 --- a/packages/block-editor/CHANGELOG.md +++ b/packages/block-editor/CHANGELOG.md @@ -9,6 +9,7 @@ ### Enhancements - `URLInput`: the `renderSuggestions` callback prop now receives `currentInputValue` as a new parameter ([45806](https://github.com/WordPress/gutenberg/pull/45806)). +- Fluid typography: add configurable fluid typography settings for minimum font size to theme.json ([#42489](https://github.com/WordPress/gutenberg/pull/42489)). ### Bug Fix diff --git a/packages/block-editor/README.md b/packages/block-editor/README.md index 5760f1584c5db8..5a861eaa1acad7 100644 --- a/packages/block-editor/README.md +++ b/packages/block-editor/README.md @@ -428,6 +428,7 @@ _Parameters_ - _args.minimumFontSize_ `?string`: Minimum font size for any clamp() calculation. Optional. - _args.scaleFactor_ `?number`: A scale factor to determine how fast a font scales within boundaries. Optional. - _args.minimumFontSizeFactor_ `?number`: How much to scale defaultFontSize by to derive minimumFontSize. Optional. +- _args.minimumFontSizeLimit_ `?string`: The smallest a calculated font size may be. Optional. _Returns_ @@ -520,7 +521,7 @@ attributes. _Parameters_ - _attributes_ `Object`: Block attributes. -- _isFluidFontSizeActive_ `boolean`: Whether the function should try to convert font sizes to fluid values. +- _fluidTypographySettings_ `Object|boolean`: If boolean, whether the function should try to convert font sizes to fluid values, otherwise an object containing theme fluid typography settings. _Returns_ 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 de8a27e3014e88..f5816e9823d7a3 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/hooks/font-size.js b/packages/block-editor/src/hooks/font-size.js index 6cb950afc45641..0c7a71fd23d683 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 00557881467ca8..12336eb2c44afd 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 d70ae08aafc593..da5869ad9aec07 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-library/src/search/edit.js b/packages/block-library/src/search/edit.js index a3db69287f8f73..78ff685ff01fe1 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/edit-site/CHANGELOG.md b/packages/edit-site/CHANGELOG.md index 83dcb848d66b3e..357668d600d11e 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/src/components/global-styles/test/typography-utils.js b/packages/edit-site/src/components/global-styles/test/typography-utils.js index 647b02cb4be1fb..9d0213cb53e554 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 a792d1875005c2..5720fdeb6ce91e 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/phpunit/block-supports/typography-test.php b/phpunit/block-supports/typography-test.php index 63fd98d1e524ff..1b9b6f97c63f6e 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/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 00000000000000..19abbecf86f4cc --- /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 00000000000000..d0ec32d9caac0a --- /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/schemas/json/theme.json b/schemas/json/theme.json index 1a8aa67b0967b1..d7349262193086 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.", From 010361fb106cc6805df7a3e81a3da4625fef7277 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Fri, 16 Dec 2022 00:01:57 +0000 Subject: [PATCH 26/51] Fix: Bug on withSafeTimeouts. (#46595) --- packages/compose/src/higher-order/with-safe-timeout/index.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 4056e64ba65e53..d15b8a72d41eab 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() { From 5e00414d2e5157d1c964717c5e38424ba7adf6bb Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Fri, 16 Dec 2022 00:09:35 +0000 Subject: [PATCH 27/51] Update: Guard against null block in off canvas editor. (#46594) * Update: Guard against null block in off canvas editor. * Change code order to comply with lint rules. --- .../src/components/off-canvas-editor/block.js | 116 +++++++++--------- 1 file changed, 60 insertions(+), 56 deletions(-) diff --git a/packages/block-editor/src/components/off-canvas-editor/block.js b/packages/block-editor/src/components/off-canvas-editor/block.js index 4ad64587796bd7..a3861ef847274f 100644 --- a/packages/block-editor/src/components/off-canvas-editor/block.js +++ b/packages/block-editor/src/components/off-canvas-editor/block.js @@ -95,15 +95,68 @@ function ListViewBlock( { [ 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( - block.name, - '__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, @@ -142,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( @@ -162,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; From 6d9b0b6313aab59bb3f071c5daf1c48fd7ce7bf1 Mon Sep 17 00:00:00 2001 From: Ramon Date: Fri, 16 Dec 2022 13:43:51 +1100 Subject: [PATCH 28/51] Site Editor: Add new 'Push changes to Global Styles' button (#46446) * Rough attempt at 'Push changes to Global Styles' button * Trying to get presets to be copied over to global styles Copy update * Styles needs default value for when only a preset is updated * Show success toast with undo button for now * Disable button when there are no changes to push * Don't create undo levels * Guard against blockGap which isn't supported * Don't show style names to user. They can't be localised * Styling tweaks * Add basic E2E test for the happy path * we need to handle an undefined argument, for example, when only a preset is selected. Getting a few Uncaught SyntaxError: "undefined" is not valid JSON for this function. * Revert package-lock.json * Adjust copy Co-authored-by: Robert Anderson --- .../global-styles/global-styles-provider.js | 16 +- .../src/components/global-styles/utils.js | 11 +- packages/edit-site/src/hooks/index.js | 1 + .../push-changes-to-global-styles/index.js | 162 ++++++++++++++++++ .../push-changes-to-global-styles/style.scss | 4 + packages/edit-site/src/style.scss | 1 + .../site-editor/push-to-global-styles.spec.js | 109 ++++++++++++ 7 files changed, 298 insertions(+), 6 deletions(-) create mode 100644 packages/edit-site/src/hooks/push-changes-to-global-styles/index.js create mode 100644 packages/edit-site/src/hooks/push-changes-to-global-styles/style.scss create mode 100644 test/e2e/specs/site-editor/push-to-global-styles.spec.js 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 0ae154c11b91af..0813dc63b2cb86 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/utils.js b/packages/edit-site/src/components/global-styles/utils.js index 06940becaf7ec1..14f3b868294172 100644 --- a/packages/edit-site/src/components/global-styles/utils.js +++ b/packages/edit-site/src/components/global-styles/utils.js @@ -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, diff --git a/packages/edit-site/src/hooks/index.js b/packages/edit-site/src/hooks/index.js index 1f7196dd2256c8..513634c55b8f01 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 00000000000000..70b9fa1b02f8ea --- /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 00000000000000..33767f4879a401 --- /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/style.scss b/packages/edit-site/src/style.scss index be5b51e16ea084..8b37e2314e237f 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/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 00000000000000..ce854f476e4f9d --- /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' ); + } ); +} ); From 0832dba18f055b8752780fdcfae3a4a9349cd5bb Mon Sep 17 00:00:00 2001 From: David Arenas Date: Fri, 16 Dec 2022 06:24:27 +0100 Subject: [PATCH 29/51] Remove unnecessary editorStyle prop in metadata (#46514) --- packages/block-library/src/comments/index.php | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/block-library/src/comments/index.php b/packages/block-library/src/comments/index.php index 69044c081c74e0..a5e51cfbf4e793 100644 --- a/packages/block-library/src/comments/index.php +++ b/packages/block-library/src/comments/index.php @@ -202,7 +202,6 @@ function register_legacy_post_comments_block() { 'wp-block-buttons', 'wp-block-button', ), - 'editorStyle' => 'wp-block-post-comments-editor', 'render_callback' => 'render_block_core_comments', 'skip_inner_blocks' => true, ); From 9c1b4575772a9a1a37cffe0ce71594a5ee6946bb Mon Sep 17 00:00:00 2001 From: Gerardo Pacheco Date: Fri, 16 Dec 2022 08:21:42 +0100 Subject: [PATCH 30/51] Mobile - Appium configutarion - Disable device animations on Android (#46535) --- packages/react-native-editor/__device-tests__/helpers/caps.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/react-native-editor/__device-tests__/helpers/caps.js b/packages/react-native-editor/__device-tests__/helpers/caps.js index fd2e8adbcb4bfa..bc776eb8aba20e 100644 --- a/packages/react-native-editor/__device-tests__/helpers/caps.js +++ b/packages/react-native-editor/__device-tests__/helpers/caps.js @@ -36,4 +36,5 @@ exports.android = { deviceOrientation: 'portrait', appiumVersion: '1.22.1', app: undefined, + disableWindowAnimation: true, }; From 6888113543212f5ceb29edd2cbfb184fece507e8 Mon Sep 17 00:00:00 2001 From: Kai Hao Date: Fri, 16 Dec 2022 15:33:47 +0800 Subject: [PATCH 31/51] Post a summary of the flaky tests to the commit (#45798) * Post a summary of the flaky tests to the commit * Add test for rendering commit comment * Update name from admin to puppeteer --- .github/workflows/end2end-test-playwright.yml | 66 ------ .github/workflows/end2end-test.yml | 97 +++++++- .github/workflows/flaky-tests.yml | 38 --- package-lock.json | 142 +---------- package.json | 2 - packages/report-flaky-tests/action.yml | 6 +- packages/report-flaky-tests/package.json | 3 +- .../__tests__/__snapshots__/run.test.ts.snap | 191 ++++++++++++++- .../src/__tests__/markdown.test.ts | 52 ++++ .../src/__tests__/run.test.ts | 222 ++++++++++++++++-- packages/report-flaky-tests/src/github-api.ts | 72 +++--- packages/report-flaky-tests/src/markdown.ts | 28 ++- packages/report-flaky-tests/src/run.ts | 70 ++++-- packages/report-flaky-tests/src/types.ts | 7 + 14 files changed, 656 insertions(+), 340 deletions(-) delete mode 100644 .github/workflows/end2end-test-playwright.yml delete mode 100644 .github/workflows/flaky-tests.yml diff --git a/.github/workflows/end2end-test-playwright.yml b/.github/workflows/end2end-test-playwright.yml deleted file mode 100644 index e0aabb17afa019..00000000000000 --- a/.github/workflows/end2end-test-playwright.yml +++ /dev/null @@ -1,66 +0,0 @@ -name: End-to-End Tests Playwright - -on: - pull_request: - push: - branches: - - trunk - - 'release/**' - - 'wp/**' - -# Cancels all previous workflow runs for pull requests that have not completed. -concurrency: - # The concurrency group contains the workflow name and the branch name for pull requests - # or the commit hash for any other events. - group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }} - cancel-in-progress: true - -jobs: - e2e: - name: E2E Tests - runs-on: ubuntu-latest - if: ${{ github.repository == 'WordPress/gutenberg' || github.event_name == 'pull_request' }} - strategy: - fail-fast: false - - steps: - - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # v3.1.0 - - - name: Use desired version of NodeJS - uses: actions/setup-node@8c91899e586c5b171469028077307d293428b516 # v3.5.1 - with: - node-version-file: '.nvmrc' - cache: npm - - - name: Npm install and build - run: | - npm ci - npm run build - - - name: Install Playwright dependencies - run: | - npx playwright install chromium firefox webkit --with-deps - - - name: Install WordPress and start the server - run: | - npm run wp-env start - - - name: Run the tests - run: | - xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run test:e2e:playwright - - - name: Archive debug artifacts (screenshots, traces) - uses: actions/upload-artifact@83fd05a356d7e2593de66fc9913b3002723633cb # v3.1.1 - if: always() - with: - name: failures-artifacts - path: artifacts/test-results - if-no-files-found: ignore - - - name: Archive flaky tests report - uses: actions/upload-artifact@83fd05a356d7e2593de66fc9913b3002723633cb # v3.1.1 - if: always() - with: - name: flaky-tests-report-playwright - path: flaky-tests - if-no-files-found: ignore diff --git a/.github/workflows/end2end-test.yml b/.github/workflows/end2end-test.yml index 172d3b090eaf0b..46278d6384a5b4 100644 --- a/.github/workflows/end2end-test.yml +++ b/.github/workflows/end2end-test.yml @@ -16,8 +16,8 @@ concurrency: cancel-in-progress: true jobs: - admin: - name: Admin - ${{ matrix.part }} + e2e-puppeteer: + name: Puppeteer - ${{ matrix.part }} runs-on: ubuntu-latest if: ${{ github.repository == 'WordPress/gutenberg' || github.event_name == 'pull_request' }} strategy: @@ -60,6 +60,97 @@ jobs: uses: actions/upload-artifact@83fd05a356d7e2593de66fc9913b3002723633cb # v3.1.1 if: always() with: - name: flaky-tests-report-${{ matrix.part }} + name: flaky-tests-report path: flaky-tests if-no-files-found: ignore + + e2e-playwright: + name: Playwright + runs-on: ubuntu-latest + if: ${{ github.repository == 'WordPress/gutenberg' || github.event_name == 'pull_request' }} + strategy: + fail-fast: false + + steps: + - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # v3.1.0 + + - name: Use desired version of NodeJS + uses: actions/setup-node@8c91899e586c5b171469028077307d293428b516 # v3.5.1 + with: + node-version-file: '.nvmrc' + cache: npm + + - name: Npm install and build + run: | + npm ci + npm run build + + - name: Install Playwright dependencies + run: | + npx playwright install chromium firefox webkit --with-deps + + - name: Install WordPress and start the server + run: | + npm run wp-env start + + - name: Run the tests + run: | + xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run test:e2e:playwright + + - name: Archive debug artifacts (screenshots, traces) + uses: actions/upload-artifact@83fd05a356d7e2593de66fc9913b3002723633cb # v3.1.1 + if: always() + with: + name: failures-artifacts + path: artifacts/test-results + if-no-files-found: ignore + + - name: Archive flaky tests report + uses: actions/upload-artifact@83fd05a356d7e2593de66fc9913b3002723633cb # v3.1.1 + if: always() + with: + name: flaky-tests-report + path: flaky-tests + if-no-files-found: ignore + + report-to-issues: + name: Report to GitHub + needs: [e2e-puppeteer, e2e-playwright] + if: ${{ always() }} + runs-on: ubuntu-latest + steps: + # Checkout defaults to using the branch which triggered the event, which + # isn't necessarily `trunk` (e.g. in the case of a merge). + - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # v3.1.0 + with: + ref: trunk + + - uses: actions/download-artifact@v3 + id: download_artifact + # Don't fail the job if there isn't any flaky tests report. + continue-on-error: true + with: + name: flaky-tests-report + path: flaky-tests + + - name: Use desired version of NodeJS + if: ${{ steps.download_artifact.outcome == 'success' }} + uses: actions/setup-node@8c91899e586c5b171469028077307d293428b516 # v3.5.1 + with: + node-version-file: '.nvmrc' + cache: npm + + - name: Npm install and build + if: ${{ steps.download_artifact.outcome == 'success' }} + # TODO: We don't have to build the entire project, just the action itself. + run: | + npm ci + npm run build:packages + + - name: Report flaky tests + if: ${{ steps.download_artifact.outcome == 'success' }} + uses: ./packages/report-flaky-tests + with: + repo-token: '${{ secrets.GITHUB_TOKEN }}' + label: '[Type] Flaky Test' + artifact-path: flaky-tests diff --git a/.github/workflows/flaky-tests.yml b/.github/workflows/flaky-tests.yml deleted file mode 100644 index 6f457f6b2292c5..00000000000000 --- a/.github/workflows/flaky-tests.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: Report Flaky Tests - -on: - workflow_run: - workflows: ['End-to-End Tests', 'End-to-End Tests Playwright'] - types: - - completed - -jobs: - report-to-issues: - name: Report to GitHub issues - runs-on: ubuntu-latest - if: ${{ github.event.workflow_run.conclusion == 'success' }} - steps: - # Checkout defaults to using the branch which triggered the event, which - # isn't necessarily `trunk` (e.g. in the case of a merge). - - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # v3.1.0 - with: - ref: trunk - - - name: Use desired version of NodeJS - uses: actions/setup-node@8c91899e586c5b171469028077307d293428b516 # v3.5.1 - with: - node-version-file: '.nvmrc' - cache: npm - - - name: Npm install and build - # TODO: We don't have to build the entire project, just the action itself. - run: | - npm ci - npm run build:packages - - - name: Report flaky tests - uses: ./packages/report-flaky-tests - with: - repo-token: '${{ secrets.GITHUB_TOKEN }}' - label: '[Type] Flaky Test' - artifact-name-prefix: flaky-tests-report diff --git a/package-lock.json b/package-lock.json index 9edebed07e4c36..7caa04085f536c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17148,15 +17148,6 @@ "integrity": "sha512-FvUupuM3rlRsRtCN+fDudtmytGO6iHJuuRKS1Ss0pG5z8oX0diNEw94UEL7hgDbpN94rgaK5R7sWm6RrSkZuAQ==", "dev": true }, - "@types/unzipper": { - "version": "0.10.5", - "resolved": "https://registry.npmjs.org/@types/unzipper/-/unzipper-0.10.5.tgz", - "integrity": "sha512-NrLJb29AdnBARpg9S/4ktfPEisbJ0AvaaAr3j7Q1tg8AgcEUsq2HqbNzvgLRoWyRtjzeLEv7vuL39u1mrNIyNA==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, "@types/uuid": { "version": "8.3.1", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.1.tgz", @@ -19167,8 +19158,7 @@ "requires": { "@actions/core": "^1.8.0", "@actions/github": "^5.0.1", - "jest-message-util": "^28.0.2", - "unzipper": "^0.10.11" + "jest-message-util": "^28.0.2" }, "dependencies": { "@actions/core": { @@ -28891,7 +28881,8 @@ "version": "1.6.48", "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.48.tgz", "integrity": "sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w==", - "dev": true + "dev": true, + "optional": true }, "big.js": { "version": "5.2.2", @@ -28937,16 +28928,6 @@ } } }, - "binary": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", - "integrity": "sha1-n2BVO8XOjDOG87VTz/R0Yq3sqnk=", - "dev": true, - "requires": { - "buffers": "~0.1.1", - "chainsaw": "~0.1.0" - } - }, "binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -29437,24 +29418,12 @@ "integrity": "sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g==", "dev": true }, - "buffer-indexof-polyfill": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz", - "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==", - "dev": true - }, "buffer-xor": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", "dev": true }, - "buffers": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", - "integrity": "sha1-skV5w77U1tOWru5tmorn9Ugqt7s=", - "dev": true - }, "builtin-modules": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", @@ -29853,23 +29822,6 @@ "integrity": "sha512-Jt9tIBkRc9POUof7QA/VwWd+58fKkEEfI+/t1/eOlxKM7ZhrczNzMFefge7Ai+39y1pR/pP6cI19guHy3FSLmw==", "dev": true }, - "chainsaw": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", - "integrity": "sha1-XqtQsor+WAdNDVgpE4iCi15fvJg=", - "dev": true, - "requires": { - "traverse": ">=0.3.0 <0.4" - }, - "dependencies": { - "traverse": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", - "integrity": "sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk=", - "dev": true - } - } - }, "chalk": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", @@ -33195,15 +33147,6 @@ "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", "dev": true }, - "duplexer2": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", - "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=", - "dev": true, - "requires": { - "readable-stream": "^2.0.2" - } - }, "duplexify": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.6.0.tgz", @@ -35021,7 +34964,7 @@ "eventemitter2": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-1.0.5.tgz", - "integrity": "sha512-EUFhWUYzqqBZlzBMI+dPU8rnKXfQZEUnitnccQuEIAnvWFHCpt3+4fts2+4dpxLtlsiseVXCMFg37KjYChSxpg==" + "integrity": "sha1-+YNhBRexc3wLncZDvsqTiTwE3xg=" }, "eventemitter3": { "version": "4.0.7", @@ -36767,43 +36710,6 @@ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "optional": true }, - "fstream": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", - "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "inherits": "~2.0.0", - "mkdirp": ">=0.5 0", - "rimraf": "2" - }, - "dependencies": { - "glob": { - "version": "7.1.7", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", - "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - } - } - }, "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", @@ -38259,7 +38165,7 @@ "htmlparser2-without-node-native": { "version": "3.9.2", "resolved": "https://registry.npmjs.org/htmlparser2-without-node-native/-/htmlparser2-without-node-native-3.9.2.tgz", - "integrity": "sha512-+FplQXqmY5fRx6vCIp2P5urWaoBCpTNJMXnKP/6mNCcyb+AZWWJzA8D03peXfozlxDL+vpgLK5dJblqEgu8j6A==", + "integrity": "sha1-s+0FDYd9D/NGWWnjOYd7f59mMfY=", "requires": { "domelementtype": "^1.3.0", "domhandler": "^2.3.0", @@ -44503,12 +44409,6 @@ } } }, - "listenercount": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", - "integrity": "sha1-hMinKrWcRyUyFIDJdeZQg0LnCTc=", - "dev": true - }, "listr": { "version": "0.14.3", "resolved": "https://registry.npmjs.org/listr/-/listr-0.14.3.tgz", @@ -60459,38 +60359,6 @@ "os-homedir": "^1.0.0" } }, - "unzipper": { - "version": "0.10.11", - "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.11.tgz", - "integrity": "sha512-+BrAq2oFqWod5IESRjL3S8baohbevGcVA+teAIOYWM3pDVdseogqbzhhvvmiyQrUNKFUnDMtELW3X8ykbyDCJw==", - "dev": true, - "requires": { - "big-integer": "^1.6.17", - "binary": "~0.3.0", - "bluebird": "~3.4.1", - "buffer-indexof-polyfill": "~1.0.0", - "duplexer2": "~0.1.4", - "fstream": "^1.0.12", - "graceful-fs": "^4.2.2", - "listenercount": "~1.0.1", - "readable-stream": "~2.3.6", - "setimmediate": "~1.0.4" - }, - "dependencies": { - "bluebird": { - "version": "3.4.7", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", - "integrity": "sha1-9y12C+Cbf3bQjtj66Ysomo0F+rM=", - "dev": true - }, - "graceful-fs": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz", - "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==", - "dev": true - } - } - }, "upath": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", diff --git a/package.json b/package.json index b1ebbf430f35fe..93c17bc1d92cdd 100644 --- a/package.json +++ b/package.json @@ -132,7 +132,6 @@ "@types/requestidlecallback": "0.3.4", "@types/semver": "7.3.8", "@types/sprintf-js": "1.1.2", - "@types/unzipper": "0.10.5", "@types/uuid": "8.3.1", "@wordpress/babel-plugin-import-jsx-pragma": "file:packages/babel-plugin-import-jsx-pragma", "@wordpress/babel-plugin-makepot": "file:packages/babel-plugin-makepot", @@ -240,7 +239,6 @@ "terser-webpack-plugin": "5.1.4", "typescript": "4.4.2", "uglify-js": "3.13.7", - "unzipper": "0.10.11", "uuid": "8.3.0", "wd": "1.12.1", "webpack": "5.65.0", diff --git a/packages/report-flaky-tests/action.yml b/packages/report-flaky-tests/action.yml index 7cea6a50c23a3f..5dfcbeb0d9fc60 100644 --- a/packages/report-flaky-tests/action.yml +++ b/packages/report-flaky-tests/action.yml @@ -8,10 +8,10 @@ inputs: description: 'The flaky-test label name' required: true default: 'flaky-test' - artifact-name-prefix: - description: 'The prefix name of the uploaded artifact' + artifact-path: + description: 'The path of the downloaded artifact' required: true - default: 'flaky-tests-report' + default: 'flaky-tests' runs: using: 'node16' main: 'build/index.js' diff --git a/packages/report-flaky-tests/package.json b/packages/report-flaky-tests/package.json index 0bb58464d33238..76528ceb8a3dbb 100644 --- a/packages/report-flaky-tests/package.json +++ b/packages/report-flaky-tests/package.json @@ -28,8 +28,7 @@ "dependencies": { "@actions/core": "^1.8.0", "@actions/github": "^5.0.1", - "jest-message-util": "^28.0.2", - "unzipper": "^0.10.11" + "jest-message-util": "^28.0.2" }, "publishConfig": { "access": "public" diff --git a/packages/report-flaky-tests/src/__tests__/__snapshots__/run.test.ts.snap b/packages/report-flaky-tests/src/__tests__/__snapshots__/run.test.ts.snap index a0a6c8a67011b9..b0ea1a4c054aa4 100644 --- a/packages/report-flaky-tests/src/__tests__/__snapshots__/run.test.ts.snap +++ b/packages/report-flaky-tests/src/__tests__/__snapshots__/run.test.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Report flaky tests should report flaky tests to issue: Created new flaky issue 1`] = ` +exports[`Report flaky tests should report flaky tests to issue on pull request: Created new flaky issue 1`] = ` " **Flaky test detected. This is an auto-generated issue by GitHub Actions. Please do NOT edit this manually.** @@ -14,7 +14,7 @@ Should insert new template part on creation
- Test passed after 2 failed attempts on headBranch. + Test passed after 2 failed attempts on headBranch. \`\`\` @@ -62,7 +62,7 @@ Should insert new template part on creation " `; -exports[`Report flaky tests should report flaky tests to issue: Updated existing flaky issue 1`] = ` +exports[`Report flaky tests should report flaky tests to issue on pull request: Updated existing flaky issue 1`] = ` " **Flaky test detected. This is an auto-generated issue by GitHub Actions. Please do NOT edit this manually.** @@ -148,7 +148,190 @@ should copy only partial selection of text blocks
- Test passed after 1 failed attempt on headBranch. + Test passed after 1 failed attempt on headBranch. + + +\`\`\` +Error: Snapshot comparison failed: + + +

block

+ + + +

B

+ + + +

A block

+ + + +

+ + + +

B block

+ + +Expected: /home/runner/work/gutenberg/gutenberg/artifacts/test-results/editor-various-copy-cut-paste-Copy-cut-paste-should-copy-only-partial-selection-of-text-blocks-chromium/Copy-cut-paste-should-copy-only-partial-selection-of-text-blocks-2-expected.txt +Received: /home/runner/work/gutenberg/gutenberg/artifacts/test-results/editor-various-copy-cut-paste-Copy-cut-paste-should-copy-only-partial-selection-of-text-blocks-chromium/Copy-cut-paste-should-copy-only-partial-selection-of-text-blocks-2-actual.txt + at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/various/copy-cut-paste.spec.js:253:52 +\`\`\` +
+ +" +`; + +exports[`Report flaky tests should report flaky tests to issue on push: Created new flaky issue 1`] = ` +" +**Flaky test detected. This is an auto-generated issue by GitHub Actions. Please do NOT edit this manually.** + +## Test title +Should insert new template part on creation + +## Test path +\`specs/site-editor/template-part.test.js\` + +## Errors + +
+ + Test passed after 2 failed attempts on trunk. + + +\`\`\` + ● Template Part › Template part block › Template part placeholder › Should insert new template part on creation + + expect(jest.fn()).not.toHaveErrored(expected) + + Expected mock function not to be called but it was called with: + [\\"TypeError: Cannot read properties of null (reading 'frameElement') + + at Vo (../../http:/localhost:8889/wp-content/plugins/gutenberg/build/block-editor/index.min.js?ver=dfd3a79ce1dc54c31b6ed591bcb0d55a:3:21925) + at ft (../../http:/localhost:8889/wp-content/plugins/gutenberg/build/vendors/react-dom.min.js?ver=6.1-alpha-53362:1:43451) + at Wt (../../http:/localhost:8889/wp-content/plugins/gutenberg/build/vendors/react-dom.min.js?ver=6.1-alpha-53362:1:50270) + at ts (../../http:/localhost:8889/wp-content/plugins/gutenberg/build/vendors/react-dom.min.js?ver=6.1-alpha-53362:1:112276) + at Fr (../../http:/localhost:8889/wp-content/plugins/gutenberg/build/vendors/react-dom.min.js?ver=6.1-alpha-53362:1:77758) + at Dr (../../http:/localhost:8889/wp-content/plugins/gutenberg/build/vendors/react-dom.min.js?ver=6.1-alpha-53362:1:77686) + at Rr (../../http:/localhost:8889/wp-content/plugins/gutenberg/build/vendors/react-dom.min.js?ver=6.1-alpha-53362:1:77549) + at Nr (../../http:/localhost:8889/wp-content/plugins/gutenberg/build/vendors/react-dom.min.js?ver=6.1-alpha-53362:1:74544) + at ../../http:/localhost:8889/wp-content/plugins/gutenberg/build/vendors/react-dom.min.js?ver=6.1-alpha-53362:1:30170 + at unstable_runWithPriority (../../http:/localhost:8889/wp-content/plugins/gutenberg/build/vendors/react.min.js?ver=6.1-alpha-53362:1:7430)\\"] + at runMicrotasks () + + ● Template Part › Template part block › Template part placeholder › Should insert new template part on creation + + expect(jest.fn()).not.toHaveErrored(expected) + + Expected mock function not to be called but it was called with: + [\\"TypeError: Cannot read properties of null (reading 'frameElement') + + at Vo (../../http:/localhost:8889/wp-content/plugins/gutenberg/build/block-editor/index.min.js?ver=dfd3a79ce1dc54c31b6ed591bcb0d55a:3:21925) + at ft (../../http:/localhost:8889/wp-content/plugins/gutenberg/build/vendors/react-dom.min.js?ver=6.1-alpha-53362:1:43451) + at Wt (../../http:/localhost:8889/wp-content/plugins/gutenberg/build/vendors/react-dom.min.js?ver=6.1-alpha-53362:1:50270) + at ts (../../http:/localhost:8889/wp-content/plugins/gutenberg/build/vendors/react-dom.min.js?ver=6.1-alpha-53362:1:112276) + at Fr (../../http:/localhost:8889/wp-content/plugins/gutenberg/build/vendors/react-dom.min.js?ver=6.1-alpha-53362:1:77758) + at Dr (../../http:/localhost:8889/wp-content/plugins/gutenberg/build/vendors/react-dom.min.js?ver=6.1-alpha-53362:1:77686) + at Rr (../../http:/localhost:8889/wp-content/plugins/gutenberg/build/vendors/react-dom.min.js?ver=6.1-alpha-53362:1:77549) + at Nr (../../http:/localhost:8889/wp-content/plugins/gutenberg/build/vendors/react-dom.min.js?ver=6.1-alpha-53362:1:74544) + at ../../http:/localhost:8889/wp-content/plugins/gutenberg/build/vendors/react-dom.min.js?ver=6.1-alpha-53362:1:30170 + at unstable_runWithPriority (../../http:/localhost:8889/wp-content/plugins/gutenberg/build/vendors/react.min.js?ver=6.1-alpha-53362:1:7430)\\"] + at runMicrotasks () + +\`\`\` +
+ +" +`; + +exports[`Report flaky tests should report flaky tests to issue on push: Updated existing flaky issue 1`] = ` +" +**Flaky test detected. This is an auto-generated issue by GitHub Actions. Please do NOT edit this manually.** + +## Test title +should copy only partial selection of text blocks + +## Test path +\`/test/e2e/specs/editor/various/copy-cut-paste.spec.js\` + +## Errors + + Test passed after 1 failed attempt on trunk. +
+ Test passed after 1 failed attempt on update/post-featured-image-component. +
+ Test passed after 1 failed attempt on add/wp-env/wp-env-core-env. +
+ Test passed after 1 failed attempt on fix/e2e-test. +
+ Test passed after 1 failed attempt on trunk. +
+ Test passed after 1 failed attempt on fix/duotone-site-editor. +
+ Test passed after 1 failed attempt on fix/playwright-snapshots. +
+ Test passed after 1 failed attempt on trunk. +
+ Test passed after 1 failed attempt on trunk. +
+ Test passed after 1 failed attempt on rnmobile/feature/drag-and-drop. +
+ Test passed after 1 failed attempt on rnmobile/feature/drag-and-drop-prevent-text-focus-on-long-press. +
+ Test passed after 1 failed attempt on add/section-concept. +
+ Test passed after 1 failed attempt on trunk. +
+ Test passed after 1 failed attempt on rnmobile/feature/drag-and-drop-refactor-draggable. +
+ Test passed after 1 failed attempt on rnmobile/feature/drag-and-drop-update-chip-animation. +
+ Test passed after 1 failed attempt on fix/comment-reply-link-alignment. +
+ Test passed after 1 failed attempt on rnmobile/feature/drag-and-drop-haptic-feedback. +
+ Test passed after 1 failed attempt on trunk. +
+ Test passed after 1 failed attempt on fix/embed-block-preview-cut-off. +
+ Test passed after 1 failed attempt on trunk. +
+ Test passed after 1 failed attempt on fix/copy-from-non-text-inputs. +
+ Test passed after 1 failed attempt on remove/block-styles-remove-role. +
+ Test passed after 1 failed attempt on docgen/replace-fixtures-with-code. +
+ Test passed after 1 failed attempt on try/use-css-var-for-user-presets. +
+ Test passed after 1 failed attempt on fix/flaky-test-reporter. +
+ Test passed after 1 failed attempt on wp/6.0. +
+ Test passed after 1 failed attempt on wp/6.0. +
+ Test passed after 1 failed attempt on wp/6.0. +
+ Test passed after 1 failed attempt on wp/6.0. +
+ Test passed after 1 failed attempt on wp/6.0. +
+ Test passed after 1 failed attempt on fix/input-field-reset-behavior-moar. +
+ Test passed after 1 failed attempt on refactor/range-control-to-typescript. +
+ Test passed after 1 failed attempt on refactor/range-control-to-typescript. +
+ Test passed after 1 failed attempt on wp/6.0. +
+ Test passed after 1 failed attempt on wp/6.0. +
+ Test passed after 1 failed attempt on wp/6.0. +
+
+ + Test passed after 1 failed attempt on trunk. \`\`\` diff --git a/packages/report-flaky-tests/src/__tests__/markdown.test.ts b/packages/report-flaky-tests/src/__tests__/markdown.test.ts index 5d7b7dd62bc873..e8a5b4f25da5d3 100644 --- a/packages/report-flaky-tests/src/__tests__/markdown.test.ts +++ b/packages/report-flaky-tests/src/__tests__/markdown.test.ts @@ -12,7 +12,10 @@ import { renderIssueBody, parseFormattedTestResults, parseIssueBody, + renderCommitComment, + isReportComment, } from '../markdown'; +import { ReportedIssue } from '../types'; jest.useFakeTimers( 'modern' ).setSystemTime( new Date( '2020-05-10' ) ); @@ -226,6 +229,55 @@ describe( 'parseIssueBody', () => { } ); } ); +describe( 'renderCommitComment', () => { + it( 'render the commit comment', () => { + const runURL = 'runURL'; + const reportedIssues: ReportedIssue[] = [ + { + testTitle: 'title1', + testPath: 'path1', + issueNumber: 1, + issueUrl: 'url1', + }, + { + testTitle: 'title2', + testPath: 'path2', + issueNumber: 2, + issueUrl: 'url2', + }, + ]; + + const commentBody = renderCommitComment( { + reportedIssues, + runURL, + } ); + + expect( commentBody ).toMatchInlineSnapshot( ` + " + **Flaky tests detected.** + Some tests passed with failed attempts. The failures may not be related to this commit but are still reported for visibility. See [the documentation](https://github.com/WordPress/gutenberg/blob/HEAD/docs/contributors/code/testing-overview.md#flaky-tests) for more information. + + 🔍 Workflow run URL: runURL + 📝 Reported issues: + - #1 in \`path1\` + - #2 in \`path2\`" + ` ); + } ); +} ); + +describe( 'isReportComment', () => { + it( 'matches the report comment', () => { + const commentBody = renderCommitComment( { + reportedIssues: [], + runURL: '', + } ); + + expect( isReportComment( commentBody ) ).toBe( true ); + + expect( isReportComment( 'random string' ) ).toBe( false ); + } ); +} ); + function renderToDisplayText( html: string ) { const container = document.createElement( 'div' ); container.innerHTML = html; diff --git a/packages/report-flaky-tests/src/__tests__/run.test.ts b/packages/report-flaky-tests/src/__tests__/run.test.ts index 93b8c4eddf46b0..b064fae579f3e3 100644 --- a/packages/report-flaky-tests/src/__tests__/run.test.ts +++ b/packages/report-flaky-tests/src/__tests__/run.test.ts @@ -2,6 +2,7 @@ * External dependencies */ import * as core from '@actions/core'; +import * as github from '@actions/github'; /** * Internal dependencies @@ -10,22 +11,42 @@ import { run } from '../run'; jest.useFakeTimers( 'modern' ).setSystemTime( new Date( '2020-05-10' ) ); -jest.mock( '@actions/github', () => ( { - context: { - repo: { - owner: 'WordPress', - repo: 'gutenberg', - }, - eventName: 'workflow_run', - payload: { - action: 'completed', - workflow_run: { - html_url: 'runURL', - head_branch: 'headBranch', - id: 100, +const mockPushEventContext = { + runId: 100, + repo: { + owner: 'WordPress', + repo: 'gutenberg', + }, + ref: 'refs/heads/trunk', + sha: 'commitSHA', + eventName: 'push', +}; +const mockPullRequestEventContext = { + runId: 100, + repo: { + owner: 'WordPress', + repo: 'gutenberg', + }, + ref: 'refs/pull/10/merge', + sha: 'mergeSHA', + eventName: 'pull_request', + payload: { + pull_request: { + head: { + ref: 'headBranch', + sha: 'headSHA', }, }, }, +}; +const mockGetContext = jest.fn( + (): typeof mockPushEventContext | typeof mockPullRequestEventContext => + mockPullRequestEventContext +); +jest.mock( '@actions/github', () => ( { + get context() { + return mockGetContext(); + }, } ) ); jest.mock( '@actions/core', () => ( { @@ -35,14 +56,19 @@ jest.mock( '@actions/core', () => ( { } ) ); const mockAPI = { - downloadReportFromArtifact: jest.fn(), fetchAllIssuesLabeledFlaky: jest.fn(), findMergeBaseCommit: jest.fn(), updateIssue: jest.fn(), createIssue: jest.fn(), + createComment: jest.fn(), }; jest.mock( '../github-api', () => ( { - GitHubAPI: jest.fn().mockImplementation( () => mockAPI ), + GitHubAPI: jest.fn( () => mockAPI ), +} ) ); + +jest.mock( 'fs/promises', () => ( { + readdir: jest.fn(), + readFile: jest.fn(), } ) ); describe( 'Report flaky tests', () => { @@ -50,7 +76,7 @@ describe( 'Report flaky tests', () => { jest.clearAllMocks(); } ); - it( 'should report flaky tests to issue', async () => { + it( 'should report flaky tests to issue on pull request', async () => { const existingFlakyTest = await import( '../__fixtures__/should copy only partial selection of text blocks.json' ).then( ( json ) => json.default ); @@ -62,8 +88,11 @@ describe( 'Report flaky tests', () => { ).then( ( json ) => json.default ); ( core.getInput as jest.Mock ) + // token .mockReturnValueOnce( 'repo-token' ) - .mockReturnValueOnce( 'flaky-tests-report' ) + // artifact-path + .mockReturnValueOnce( 'flaky-tests' ) + // label .mockReturnValueOnce( '[Type] Flaky Test' ); // Replacing the cwd for the test for consistent snapshot results. @@ -71,29 +100,146 @@ describe( 'Report flaky tests', () => { '/home/runner/work/gutenberg/gutenberg', process.cwd() ); - mockAPI.downloadReportFromArtifact.mockImplementationOnce( () => [ - existingFlakyTest, - newFlakyTest, - ] ); + + const mockedFs = require( 'fs/promises' ); + mockedFs.readdir.mockImplementationOnce( () => + Promise.resolve( [ + `${ existingFlakyTest.title }.json`, + `${ newFlakyTest.title }.json`, + ] ) + ); + mockedFs.readFile + .mockImplementationOnce( () => + Promise.resolve( JSON.stringify( existingFlakyTest ) ) + ) + .mockImplementationOnce( () => + Promise.resolve( JSON.stringify( newFlakyTest ) ) + ); mockAPI.fetchAllIssuesLabeledFlaky.mockImplementationOnce( () => flakyIssues ); mockAPI.updateIssue.mockImplementationOnce( () => ( { + number: 1, html_url: 'html_url', } ) ); mockAPI.createIssue.mockImplementationOnce( () => ( { + number: 2, html_url: 'html_url', } ) ); + mockAPI.createComment.mockImplementationOnce( () => ( { + html_url: 'comment_html_url', + } ) ); + await run(); const existingFlakyIssue = flakyIssues.find( ( issue ) => issue.title === `[Flaky Test] ${ existingFlakyTest.title }` + )!; + expect( mockAPI.updateIssue ).toHaveBeenCalledWith( + expect.objectContaining( { + issue_number: existingFlakyIssue.number, + state: 'open', + } ) + ); + expect( mockAPI.updateIssue.mock.calls[ 0 ][ 0 ].body ).toMatchSnapshot( + 'Updated existing flaky issue' ); + + expect( mockAPI.createIssue ).toHaveBeenCalledWith( + expect.objectContaining( { + title: `[Flaky Test] ${ newFlakyTest.title }`, + } ) + ); + expect( mockAPI.createIssue.mock.calls[ 0 ][ 0 ].body ).toMatchSnapshot( + 'Created new flaky issue' + ); + + expect( mockAPI.createComment ).toHaveBeenCalledTimes( 1 ); + expect( mockAPI.createComment.mock.calls[ 0 ][ 0 ] ).toBe( 'headSHA' ); + expect( mockAPI.createComment.mock.calls[ 0 ][ 1 ] ) + .toMatchInlineSnapshot( ` + " + **Flaky tests detected.** + Some tests passed with failed attempts. The failures may not be related to this commit but are still reported for visibility. See [the documentation](https://github.com/WordPress/gutenberg/blob/HEAD/docs/contributors/code/testing-overview.md#flaky-tests) for more information. + + 🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/100 + 📝 Reported issues: + - #1 in \`/test/e2e/specs/editor/various/copy-cut-paste.spec.js\` + - #2 in \`specs/site-editor/template-part.test.js\`" + ` ); + } ); + + it( 'should report flaky tests to issue on push', async () => { + mockGetContext.mockImplementation( () => mockPushEventContext ); + + const existingFlakyTest = await import( + '../__fixtures__/should copy only partial selection of text blocks.json' + ).then( ( json ) => json.default ); + const newFlakyTest = await import( + '../__fixtures__/Should insert new template part on creation.json' + ).then( ( json ) => json.default ); + const flakyIssues = await import( + '../__fixtures__/flaky-issues.json' + ).then( ( json ) => json.default ); + + ( core.getInput as jest.Mock ) + // token + .mockReturnValueOnce( 'repo-token' ) + // artifact-path + .mockReturnValueOnce( 'flaky-tests' ) + // label + .mockReturnValueOnce( '[Type] Flaky Test' ); + + // Replacing the cwd for the test for consistent snapshot results. + existingFlakyTest.path = existingFlakyTest.path.replace( + '/home/runner/work/gutenberg/gutenberg', + process.cwd() + ); + + const mockedFs = require( 'fs/promises' ); + mockedFs.readdir.mockImplementationOnce( () => + Promise.resolve( [ + `${ existingFlakyTest.title }.json`, + `${ newFlakyTest.title }.json`, + ] ) + ); + mockedFs.readFile + .mockImplementationOnce( () => + Promise.resolve( JSON.stringify( existingFlakyTest ) ) + ) + .mockImplementationOnce( () => + Promise.resolve( JSON.stringify( newFlakyTest ) ) + ); + + mockAPI.fetchAllIssuesLabeledFlaky.mockImplementationOnce( + () => flakyIssues + ); + + mockAPI.updateIssue.mockImplementationOnce( () => ( { + number: 1, + html_url: 'html_url', + } ) ); + + mockAPI.createIssue.mockImplementationOnce( () => ( { + number: 2, + html_url: 'html_url', + } ) ); + + mockAPI.createComment.mockImplementationOnce( () => ( { + html_url: 'comment_html_url', + } ) ); + + await run(); + + const existingFlakyIssue = flakyIssues.find( + ( issue ) => + issue.title === `[Flaky Test] ${ existingFlakyTest.title }` + )!; expect( mockAPI.updateIssue ).toHaveBeenCalledWith( expect.objectContaining( { issue_number: existingFlakyIssue.number, @@ -112,6 +258,24 @@ describe( 'Report flaky tests', () => { expect( mockAPI.createIssue.mock.calls[ 0 ][ 0 ].body ).toMatchSnapshot( 'Created new flaky issue' ); + + expect( mockAPI.createComment ).toHaveBeenCalledTimes( 1 ); + expect( mockAPI.createComment.mock.calls[ 0 ][ 0 ] ).toBe( + 'commitSHA' + ); + expect( mockAPI.createComment.mock.calls[ 0 ][ 1 ] ) + .toMatchInlineSnapshot( ` + " + **Flaky tests detected.** + Some tests passed with failed attempts. The failures may not be related to this commit but are still reported for visibility. See [the documentation](https://github.com/WordPress/gutenberg/blob/HEAD/docs/contributors/code/testing-overview.md#flaky-tests) for more information. + + 🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/100 + 📝 Reported issues: + - #1 in \`/test/e2e/specs/editor/various/copy-cut-paste.spec.js\` + - #2 in \`specs/site-editor/template-part.test.js\`" + ` ); + + mockGetContext.mockImplementation( () => mockPullRequestEventContext ); } ); it( 'should skip for outdated branches', async () => { @@ -132,9 +296,14 @@ describe( 'Report flaky tests', () => { '/home/runner/work/gutenberg/gutenberg', process.cwd() ); - mockAPI.downloadReportFromArtifact.mockImplementationOnce( () => [ - flakyTest, - ] ); + + const mockedFs = require( 'fs/promises' ); + mockedFs.readdir.mockImplementationOnce( () => + Promise.resolve( [ `${ flakyTest.title }.json` ] ) + ); + mockedFs.readFile.mockImplementationOnce( () => + Promise.resolve( JSON.stringify( flakyTest ) ) + ); mockAPI.fetchAllIssuesLabeledFlaky.mockImplementationOnce( () => flakyIssues @@ -151,8 +320,9 @@ describe( 'Report flaky tests', () => { // indicating that the code base at this point is outdated. const flakyIssue = flakyIssues.find( ( issue ) => issue.title === `[Flaky Test] ${ flakyTest.title }` - ); + )!; flakyIssue.state = 'closed'; + // @ts-expect-error: "closed_at" hasn't been typed yet. flakyIssue.closed_at = new Date( '2022-05-15' ).toISOString(); await run(); @@ -160,5 +330,7 @@ describe( 'Report flaky tests', () => { expect( mockAPI.findMergeBaseCommit ).toHaveBeenCalledTimes( 1 ); expect( mockAPI.updateIssue ).not.toHaveBeenCalled(); + + expect( mockAPI.createComment ).not.toHaveBeenCalled(); } ); } ); diff --git a/packages/report-flaky-tests/src/github-api.ts b/packages/report-flaky-tests/src/github-api.ts index b7aa0990085683..8d5d49025f576c 100644 --- a/packages/report-flaky-tests/src/github-api.ts +++ b/packages/report-flaky-tests/src/github-api.ts @@ -2,14 +2,13 @@ * External dependencies */ import { getOctokit } from '@actions/github'; -import * as unzipper from 'unzipper'; import type { GitHub } from '@actions/github/lib/utils'; import type { Endpoints } from '@octokit/types'; /** * Internal dependencies */ -import type { FlakyTestResult } from './types'; +import { isReportComment } from './markdown'; type Octokit = InstanceType< typeof GitHub >; @@ -27,44 +26,6 @@ class GitHubAPI { this.#repo = repo; } - async downloadReportFromArtifact( - runID: number, - artifactNamePrefix: string - ): Promise< FlakyTestResult[] | undefined > { - const { - data: { artifacts }, - } = await this.#octokit.rest.actions.listWorkflowRunArtifacts( { - ...this.#repo, - run_id: runID, - } ); - - const matchArtifact = artifacts.find( ( artifact ) => - artifact.name.startsWith( artifactNamePrefix ) - ); - - if ( ! matchArtifact ) { - return undefined; - } - - const download = await this.#octokit.rest.actions.downloadArtifact( { - ...this.#repo, - artifact_id: matchArtifact.id, - archive_format: 'zip', - } ); - - const { files } = await unzipper.Open.buffer( - Buffer.from( download.data as Buffer ) - ); - const fileBuffers = await Promise.all( - files.map( ( file ) => file.buffer() ) - ); - const parsedFiles = fileBuffers.map( - ( buffer ) => JSON.parse( buffer.toString() ) as FlakyTestResult - ); - - return parsedFiles; - } - async fetchAllIssuesLabeledFlaky( label: string ) { const issues = await this.#octokit.paginate( this.#octokit.rest.issues.listForRepo, @@ -116,6 +77,37 @@ class GitHubAPI { return data; } + + async createComment( sha: string, body: string ) { + const { data: comments } = + await this.#octokit.rest.repos.listCommentsForCommit( { + ...this.#repo, + commit_sha: sha, + } ); + const reportComment = comments.find( ( comment ) => + isReportComment( comment.body ) + ); + + if ( reportComment ) { + const { data } = await this.#octokit.rest.repos.updateCommitComment( + { + ...this.#repo, + comment_id: reportComment.id, + body, + } + ); + + return data; + } + + const { data } = await this.#octokit.rest.repos.createCommitComment( { + ...this.#repo, + commit_sha: sha, + body, + } ); + + return data; + } } export { GitHubAPI }; diff --git a/packages/report-flaky-tests/src/markdown.ts b/packages/report-flaky-tests/src/markdown.ts index 2852623115c674..15d850773ffe65 100644 --- a/packages/report-flaky-tests/src/markdown.ts +++ b/packages/report-flaky-tests/src/markdown.ts @@ -9,7 +9,7 @@ import * as core from '@actions/core'; * Internal dependencies */ import { stripAnsi } from './strip-ansi'; -import type { MetaData, FlakyTestResult } from './types'; +import type { MetaData, FlakyTestResult, ReportedIssue } from './types'; type ParsedTestResult = { date: Date; @@ -203,10 +203,36 @@ function parseIssueBody( body: string ) { }; } +const FLAKY_TESTS_REPORT_COMMENT_TOKEN = `flaky-tests-report-comment`; + +function renderCommitComment( { + reportedIssues, + runURL, +}: { + reportedIssues: ReportedIssue[]; + runURL: string; +} ) { + return ` +**Flaky tests detected.** +Some tests passed with failed attempts. The failures may not be related to this commit but are still reported for visibility. See [the documentation](https://github.com/WordPress/gutenberg/blob/HEAD/docs/contributors/code/testing-overview.md#flaky-tests) for more information. + +🔍 Workflow run URL: ${ runURL } +📝 Reported issues: +${ reportedIssues + .map( ( issue ) => `- #${ issue.issueNumber } in \`${ issue.testPath }\`` ) + .join( '\n' ) }`; +} + +function isReportComment( body: string ) { + return body.startsWith( `` ); +} + export { renderIssueBody, formatTestErrorMessage, formatTestResults, parseFormattedTestResults, parseIssueBody, + renderCommitComment, + isReportComment, }; diff --git a/packages/report-flaky-tests/src/run.ts b/packages/report-flaky-tests/src/run.ts index 307a41cb100787..65323c3d87fa80 100644 --- a/packages/report-flaky-tests/src/run.ts +++ b/packages/report-flaky-tests/src/run.ts @@ -3,7 +3,9 @@ */ import * as github from '@actions/github'; import * as core from '@actions/core'; -import type { WorkflowRunCompletedEvent } from '@octokit/webhooks-types'; +import * as fs from 'fs/promises'; +import * as path from 'path'; +import type { PullRequestEvent } from '@octokit/webhooks-types'; /** * Internal dependencies @@ -14,39 +16,44 @@ import { formatTestErrorMessage, formatTestResults, parseIssueBody, + renderCommitComment, } from './markdown'; +import type { ReportedIssue } from './types'; async function run() { - if ( - github.context.eventName !== 'workflow_run' || - github.context.payload.action !== 'completed' - ) { - return; - } - const token = core.getInput( 'repo-token', { required: true } ); - const artifactNamePrefix = core.getInput( 'artifact-name-prefix', { + const artifactPath = core.getInput( 'artifact-path', { required: true, } ); - const api = new GitHubAPI( token, github.context.repo ); - // Cast the payload type: https://github.com/actions/toolkit/tree/main/packages/github#webhook-payload-typescript-definitions - const { - workflow_run: { head_branch: headBranch, html_url: runURL, id: runID }, - } = github.context.payload as WorkflowRunCompletedEvent; - - const flakyTests = await api.downloadReportFromArtifact( - runID, - artifactNamePrefix + const { runId: runID, repo, ref } = github.context; + const runURL = `https://github.com/${ repo.owner }/${ repo.repo }/actions/runs/${ runID }`; + const api = new GitHubAPI( token, repo ); + + const flakyTestsDir = await fs.readdir( artifactPath ); + const flakyTests = await Promise.all( + flakyTestsDir.map( ( filename ) => + fs + .readFile( path.join( artifactPath, filename ), 'utf-8' ) + .then( ( text ) => JSON.parse( text ) ) + ) ); - if ( ! flakyTests ) { + if ( ! flakyTests || flakyTests.length === 0 ) { // No flaky tests reported in this run. return; } + const headBranch = + github.context.eventName === 'pull_request' + ? // Cast the payload type: https://github.com/actions/toolkit/tree/main/packages/github#webhook-payload-typescript-definitions + ( github.context.payload as PullRequestEvent ).pull_request.head + .ref + : ref.replace( /^refs\/(heads|tag)\//, '' ); + const label = core.getInput( 'label', { required: true } ); const issues = await api.fetchAllIssuesLabeledFlaky( label ); + const reportedIssues: ReportedIssue[] = []; for ( const flakyTest of flakyTests ) { const { title: testTitle } = flakyTest; @@ -143,8 +150,33 @@ async function run() { } ); } + reportedIssues.push( { + testTitle, + testPath, + issueNumber: issue.number, + issueUrl: issue.html_url, + } ); core.info( `Reported flaky test to ${ issue.html_url }` ); } + + if ( reportedIssues.length === 0 ) { + return; + } + + const commitSHA = + github.context.eventName === 'pull_request' + ? // Cast the payload type: https://github.com/actions/toolkit/tree/main/packages/github#webhook-payload-typescript-definitions + ( github.context.payload as PullRequestEvent ).pull_request.head + .sha + : github.context.sha; + const { html_url: commentUrl } = await api.createComment( + commitSHA, + renderCommitComment( { + runURL, + reportedIssues, + } ) + ); + core.info( `Reported the summary of the flaky tests to ${ commentUrl }` ); } function getIssueTitle( testTitle: string ) { diff --git a/packages/report-flaky-tests/src/types.ts b/packages/report-flaky-tests/src/types.ts index e52d6dba471018..5de8770ef75418 100644 --- a/packages/report-flaky-tests/src/types.ts +++ b/packages/report-flaky-tests/src/types.ts @@ -29,3 +29,10 @@ export type MetaData = { totalCommits?: number; baseCommit?: string; }; + +export type ReportedIssue = { + testTitle: string; + testPath: string; + issueNumber: number; + issueUrl: string; +}; From ff9ec58a17c596aa9e57f26736bca38d24bcf7e7 Mon Sep 17 00:00:00 2001 From: George Mamadashvili Date: Fri, 16 Dec 2022 13:33:06 +0400 Subject: [PATCH 32/51] Site Editor: Memoize sidebar component (#46604) --- packages/edit-site/src/components/layout/index.js | 2 +- packages/edit-site/src/components/sidebar/index.js | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/edit-site/src/components/layout/index.js b/packages/edit-site/src/components/layout/index.js index e3d2edc217eefd..e451357ee97e5c 100644 --- a/packages/edit-site/src/components/layout/index.js +++ b/packages/edit-site/src/components/layout/index.js @@ -28,7 +28,7 @@ import { store as keyboardShortcutsStore } from '@wordpress/keyboard-shortcuts'; /** * Internal dependencies */ -import { Sidebar } from '../sidebar'; +import Sidebar from '../sidebar'; import Editor from '../editor'; import ListPage from '../list'; import ErrorBoundary from '../error-boundary'; diff --git a/packages/edit-site/src/components/sidebar/index.js b/packages/edit-site/src/components/sidebar/index.js index 06abec6f024cac..1a0f2bcfb6f2b8 100644 --- a/packages/edit-site/src/components/sidebar/index.js +++ b/packages/edit-site/src/components/sidebar/index.js @@ -1,6 +1,7 @@ /** * WordPress dependencies */ +import { memo } from '@wordpress/element'; import { __experimentalNavigatorProvider as NavigatorProvider } from '@wordpress/components'; /** @@ -22,7 +23,7 @@ function SidebarScreens() { ); } -export function Sidebar() { +function Sidebar() { return ( ); } + +export default memo( Sidebar ); From f8efb0d0604ad628227ff9bf725a6dcaf4e87b6a Mon Sep 17 00:00:00 2001 From: Jarda Snajdr Date: Fri, 16 Dec 2022 10:57:52 +0100 Subject: [PATCH 33/51] package-lock.json: dedupe the scheduler package (#46605) --- package-lock.json | 33 +++++++++++---------------------- 1 file changed, 11 insertions(+), 22 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7caa04085f536c..9906e5f5bac5c5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -52602,16 +52602,6 @@ "requires": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" - }, - "dependencies": { - "scheduler": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", - "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", - "requires": { - "loose-envify": "^1.1.0" - } - } } }, "react-easy-crop": { @@ -52998,6 +52988,14 @@ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.4.3.tgz", "integrity": "sha512-Hwln1VNuGl/6bVwnd0Xdn1e84gT/8T9aYNL+HAKDArLCS7LWjwr7StE30IEYbIkx0Vi3vs+coQxe+SQDbGbbpA==" }, + "scheduler": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.21.0.tgz", + "integrity": "sha512-1r87x5fz9MXqswA2ERLo0EbOAU74DpIUO090gIasYTqlVoJeMcl+Z1Rg7WHz+qtPujhS/hGIt9kxZOYBV3faRQ==", + "requires": { + "loose-envify": "^1.1.0" + } + }, "semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", @@ -53654,15 +53652,6 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", "dev": true - }, - "scheduler": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", - "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", - "dev": true, - "requires": { - "loose-envify": "^1.1.0" - } } } }, @@ -55511,9 +55500,9 @@ } }, "scheduler": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.21.0.tgz", - "integrity": "sha512-1r87x5fz9MXqswA2ERLo0EbOAU74DpIUO090gIasYTqlVoJeMcl+Z1Rg7WHz+qtPujhS/hGIt9kxZOYBV3faRQ==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", "requires": { "loose-envify": "^1.1.0" } From d343bd5dd95c55911dc66e06d130ae07cd219331 Mon Sep 17 00:00:00 2001 From: Marin Atanasov <8436925+tyxla@users.noreply.github.com> Date: Fri, 16 Dec 2022 12:46:24 +0200 Subject: [PATCH 34/51] Lodash: Refactor block editor away from _.find() (#46577) --- .eslintrc.js | 1 + .../src/components/alignment-control/ui.js | 8 +------- .../src/components/block-styles/index.native.js | 3 +-- .../src/components/block-styles/utils.js | 12 +++++------- .../block-tools/selected-block-popover.js | 4 +--- .../block-editor/src/components/colors/utils.js | 8 +++++--- .../src/components/font-sizes/utils.js | 8 +++++--- .../src/components/font-sizes/with-font-sizes.js | 8 ++++---- .../src/components/gradients/use-gradient.js | 9 ++------- .../src/components/inserter/search-items.js | 3 +-- .../src/components/rich-text/format-edit.js | 16 ++++++---------- .../src/components/rich-text/index.js | 1 + .../components/url-popover/image-url-input-ui.js | 9 +++++---- packages/block-editor/src/hooks/font-family.js | 8 +++----- packages/block-editor/src/store/selectors.js | 5 ++--- 15 files changed, 43 insertions(+), 60 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 51bca0f3bc059b..3f2e466a78ff25 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -68,6 +68,7 @@ const restrictedImports = [ 'every', 'extend', 'filter', + 'find', 'findIndex', 'findKey', 'findLast', diff --git a/packages/block-editor/src/components/alignment-control/ui.js b/packages/block-editor/src/components/alignment-control/ui.js index f50e69044f9e59..171a8d34c45f5c 100644 --- a/packages/block-editor/src/components/alignment-control/ui.js +++ b/packages/block-editor/src/components/alignment-control/ui.js @@ -1,8 +1,3 @@ -/** - * External dependencies - */ -import { find } from 'lodash'; - /** * WordPress dependencies */ @@ -46,8 +41,7 @@ function AlignmentUI( { return () => onChange( value === align ? undefined : align ); } - const activeAlignment = find( - alignmentControls, + const activeAlignment = alignmentControls.find( ( control ) => control.align === value ); diff --git a/packages/block-editor/src/components/block-styles/index.native.js b/packages/block-editor/src/components/block-styles/index.native.js index 142deafacfa7f1..7967e79c2c1cd8 100644 --- a/packages/block-editor/src/components/block-styles/index.native.js +++ b/packages/block-editor/src/components/block-styles/index.native.js @@ -2,7 +2,6 @@ * External dependencies */ import { ScrollView } from 'react-native'; -import { find } from 'lodash'; /** * WordPress dependencies @@ -35,7 +34,7 @@ function BlockStyles( { clientId, url } ) { const { updateBlockAttributes } = useDispatch( blockEditorStore ); - const renderedStyles = find( styles, 'isDefault' ) + const renderedStyles = styles?.find( ( style ) => style.isDefault ) ? styles : [ { diff --git a/packages/block-editor/src/components/block-styles/utils.js b/packages/block-editor/src/components/block-styles/utils.js index f60a23a4287b21..511e78da83da60 100644 --- a/packages/block-editor/src/components/block-styles/utils.js +++ b/packages/block-editor/src/components/block-styles/utils.js @@ -1,7 +1,3 @@ -/** - * External dependencies - */ -import { find } from 'lodash'; /** * WordPress dependencies */ @@ -23,13 +19,15 @@ export function getActiveStyle( styles, className ) { } const potentialStyleName = style.substring( 9 ); - const activeStyle = find( styles, { name: potentialStyleName } ); + const activeStyle = styles?.find( + ( { name } ) => name === potentialStyleName + ); if ( activeStyle ) { return activeStyle; } } - return find( styles, 'isDefault' ); + return getDefaultStyle( styles ); } /** @@ -88,5 +86,5 @@ export function getRenderedStyles( styles ) { * @return {Object?} The default style object, if found. */ export function getDefaultStyle( styles ) { - return find( styles, 'isDefault' ); + return styles?.find( ( style ) => style.isDefault ); } diff --git a/packages/block-editor/src/components/block-tools/selected-block-popover.js b/packages/block-editor/src/components/block-tools/selected-block-popover.js index 343cd50058655a..294813b1f34a15 100644 --- a/packages/block-editor/src/components/block-tools/selected-block-popover.js +++ b/packages/block-editor/src/components/block-tools/selected-block-popover.js @@ -1,7 +1,6 @@ /** * External dependencies */ -import { find } from 'lodash'; import classnames from 'classnames'; /** @@ -240,8 +239,7 @@ function wrapperSelector( select ) { ); // Get the clientId of the topmost parent with the capture toolbars setting. - const capturingClientId = find( - blockParentsClientIds, + const capturingClientId = blockParentsClientIds.find( ( parentClientId ) => parentBlockListSettings[ parentClientId ] ?.__experimentalCaptureToolbars diff --git a/packages/block-editor/src/components/colors/utils.js b/packages/block-editor/src/components/colors/utils.js index e22b5ed8661d36..e48b599faa6b96 100644 --- a/packages/block-editor/src/components/colors/utils.js +++ b/packages/block-editor/src/components/colors/utils.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { find, kebabCase } from 'lodash'; +import { kebabCase } from 'lodash'; import { colord, extend } from 'colord'; import namesPlugin from 'colord/plugins/names'; import a11yPlugin from 'colord/plugins/a11y'; @@ -26,7 +26,9 @@ export const getColorObjectByAttributeValues = ( customColor ) => { if ( definedColor ) { - const colorObj = find( colors, { slug: definedColor } ); + const colorObj = colors?.find( + ( color ) => color.slug === definedColor + ); if ( colorObj ) { return colorObj; @@ -47,7 +49,7 @@ export const getColorObjectByAttributeValues = ( * Returns undefined if no color object matches this requirement. */ export const getColorObjectByColorValue = ( colors, colorValue ) => { - return find( colors, { color: colorValue } ); + return colors?.find( ( color ) => color.color === colorValue ); }; /** diff --git a/packages/block-editor/src/components/font-sizes/utils.js b/packages/block-editor/src/components/font-sizes/utils.js index 1237a13efd7db6..0948ab918f156c 100644 --- a/packages/block-editor/src/components/font-sizes/utils.js +++ b/packages/block-editor/src/components/font-sizes/utils.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { find, kebabCase } from 'lodash'; +import { kebabCase } from 'lodash'; /** * Returns the font size object based on an array of named font sizes and the namedFontSize and customFontSize values. @@ -20,7 +20,9 @@ export const getFontSize = ( customFontSizeAttribute ) => { if ( fontSizeAttribute ) { - const fontSizeObject = find( fontSizes, { slug: fontSizeAttribute } ); + const fontSizeObject = fontSizes?.find( + ( { slug } ) => slug === fontSizeAttribute + ); if ( fontSizeObject ) { return fontSizeObject; } @@ -39,7 +41,7 @@ export const getFontSize = ( * @return {Object} Font size object. */ export function getFontSizeObjectByValue( fontSizes, value ) { - const fontSizeObject = find( fontSizes, { size: value } ); + const fontSizeObject = fontSizes?.find( ( { size } ) => size === value ); if ( fontSizeObject ) { return fontSizeObject; } diff --git a/packages/block-editor/src/components/font-sizes/with-font-sizes.js b/packages/block-editor/src/components/font-sizes/with-font-sizes.js index e328a0b2740385..380ea5a3dc0c5c 100644 --- a/packages/block-editor/src/components/font-sizes/with-font-sizes.js +++ b/packages/block-editor/src/components/font-sizes/with-font-sizes.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { find, pickBy } from 'lodash'; +import { pickBy } from 'lodash'; /** * WordPress dependencies @@ -107,9 +107,9 @@ export default ( ...fontSizeNames ) => { customFontSizeAttributeName ) { return ( fontSizeValue ) => { - const fontSizeObject = find( this.props.fontSizes, { - size: Number( fontSizeValue ), - } ); + const fontSizeObject = this.props.fontSizes?.find( + ( { size } ) => size === Number( fontSizeValue ) + ); this.props.setAttributes( { [ fontSizeAttributeName ]: fontSizeObject && fontSizeObject.slug diff --git a/packages/block-editor/src/components/gradients/use-gradient.js b/packages/block-editor/src/components/gradients/use-gradient.js index 4acf09fc03b522..e899f9c9197708 100644 --- a/packages/block-editor/src/components/gradients/use-gradient.js +++ b/packages/block-editor/src/components/gradients/use-gradient.js @@ -1,8 +1,3 @@ -/** - * External dependencies - */ -import { find } from 'lodash'; - /** * WordPress dependencies */ @@ -32,7 +27,7 @@ export function __experimentalGetGradientClass( gradientSlug ) { * @return {string} Gradient value. */ export function getGradientValueBySlug( gradients, slug ) { - const gradient = find( gradients, [ 'slug', slug ] ); + const gradient = gradients?.find( ( g ) => g.slug === slug ); return gradient && gradient.gradient; } @@ -40,7 +35,7 @@ export function __experimentalGetGradientObjectByGradientValue( gradients, value ) { - const gradient = find( gradients, [ 'gradient', value ] ); + const gradient = gradients?.find( ( g ) => g.gradient === value ); return gradient; } diff --git a/packages/block-editor/src/components/inserter/search-items.js b/packages/block-editor/src/components/inserter/search-items.js index 5451f19937b01d..35346fca09f32d 100644 --- a/packages/block-editor/src/components/inserter/search-items.js +++ b/packages/block-editor/src/components/inserter/search-items.js @@ -2,7 +2,6 @@ * External dependencies */ import removeAccents from 'remove-accents'; -import { find } from 'lodash'; import { noCase } from 'change-case'; // Default search helpers. @@ -88,7 +87,7 @@ export const searchBlockItems = ( const config = { getCategory: ( item ) => - find( categories, { slug: item.category } )?.title, + categories.find( ( { slug } ) => slug === item.category )?.title, getCollection: ( item ) => collections[ item.name.split( '/' )[ 0 ] ]?.title, }; diff --git a/packages/block-editor/src/components/rich-text/format-edit.js b/packages/block-editor/src/components/rich-text/format-edit.js index 3c6861c341cd4b..3ab3f47d226aec 100644 --- a/packages/block-editor/src/components/rich-text/format-edit.js +++ b/packages/block-editor/src/components/rich-text/format-edit.js @@ -6,10 +6,6 @@ import { getActiveObject, isCollapsed, } from '@wordpress/rich-text'; -/** - * External dependencies - */ -import { find } from 'lodash'; export default function FormatEdit( { formatTypes, @@ -40,13 +36,13 @@ export default function FormatEdit( { if ( name === 'core/link' && ! isCollapsed( value ) ) { const formats = value.formats; - const linkFormatAtStart = find( formats[ value.start ], { - type: 'core/link', - } ); + const linkFormatAtStart = formats[ value.start ]?.find( + ( { type } ) => type === 'core/link' + ); - const linkFormatAtEnd = find( formats[ value.end - 1 ], { - type: 'core/link', - } ); + const linkFormatAtEnd = formats[ value.end - 1 ]?.find( + ( { type } ) => type === 'core/link' + ); if ( ! linkFormatAtStart || diff --git a/packages/block-editor/src/components/rich-text/index.js b/packages/block-editor/src/components/rich-text/index.js index 726f8cddfff7b7..8219272ad3043a 100644 --- a/packages/block-editor/src/components/rich-text/index.js +++ b/packages/block-editor/src/components/rich-text/index.js @@ -368,6 +368,7 @@ function RichTextWrapper( { children && children( { value, onChange, onFocus } ) } + { + linkDestinations.find( ( destination ) => { return destination.url === value; } ) || { linkDestination: LINK_DESTINATION_CUSTOM } ).linkDestination; @@ -236,8 +236,9 @@ const ImageURLInputUI = ( { const linkEditorValue = urlInput !== null ? urlInput : url; const urlLabel = ( - find( getLinkDestinations(), [ 'linkDestination', linkDestination ] ) || - {} + getLinkDestinations().find( + ( destination ) => destination.linkDestination === linkDestination + ) || {} ).title; return ( diff --git a/packages/block-editor/src/hooks/font-family.js b/packages/block-editor/src/hooks/font-family.js index fe8693f64421bf..56335ced887074 100644 --- a/packages/block-editor/src/hooks/font-family.js +++ b/packages/block-editor/src/hooks/font-family.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { find, kebabCase } from 'lodash'; +import { kebabCase } from 'lodash'; /** * WordPress dependencies @@ -111,14 +111,12 @@ export function FontFamilyEdit( { } ) { const fontFamilies = useSetting( 'typography.fontFamilies' ); - const value = find( - fontFamilies, + const value = fontFamilies?.find( ( { slug } ) => fontFamily === slug )?.fontFamily; function onChange( newValue ) { - const predefinedFontFamily = find( - fontFamilies, + const predefinedFontFamily = fontFamilies?.find( ( { fontFamily: f } ) => f === newValue ); setAttributes( { diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index 3c048a7d58a29c..f096767bf6178c 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { map, find } from 'lodash'; +import { map } from 'lodash'; import createSelector from 'rememo'; /** @@ -2469,8 +2469,7 @@ export const __experimentalGetBlockListSettingsForBlocks = createSelector( */ export const __experimentalGetReusableBlockTitle = createSelector( ( state, ref ) => { - const reusableBlock = find( - getReusableBlocks( state ), + const reusableBlock = getReusableBlocks( state ).find( ( block ) => block.id === ref ); if ( ! reusableBlock ) { From c465d705d969e5d556ac24960304e47d1148072f Mon Sep 17 00:00:00 2001 From: Gerardo Pacheco Date: Fri, 16 Dec 2022 12:06:24 +0100 Subject: [PATCH 35/51] [Mobile] Update E2E tests by removing some in favor of integration tests (#46583) * Mobile - Remove Columns E2E test since it's already covered in integration tests * Mobile - Remove Latest Post E2E in favor of new integration test * Mobile - Remove disabled List block E2E tests since it was added for the previous version of the block and new integration tests were added to support List Block V2 * Mobile - Remove More block E2E test in favor of new integration test * Mobile - Remove Verse block E2E test in favor of new integration tests * Mobile - Remove Spacer block E2E test since it's already covered in integration tests * Mobile - Remove Separator block E2E test in favor of new integration test * Mobile - Appium configutarion - Disable device animations on Android * Mobile - E2E tests - Ignore Canary tests in Full tests suite, since it will be run in a different job (device-tests-canary) --- .../test/__snapshots__/edit.native.js.snap | 3 + .../src/latest-posts/test/edit.native.js | 49 +++++++++++++ .../test/__snapshots__/edit.native.js.snap | 7 ++ .../src/more/test/edit.native.js | 41 +++++++++++ .../test/__snapshots__/edit.native.js.snap | 7 ++ .../src/separator/test/edit.native.js | 41 +++++++++++ .../test/__snapshots__/edit.native.js.snap | 13 ++++ .../src/verse/test/edit.native.js | 73 ++++++++++--------- .../gutenberg-editor-columns.test.js | 19 ----- .../gutenberg-editor-latest-posts.test.js | 16 ---- .../gutenberg-editor-lists-@canary.test.js | 61 ---------------- .../gutenberg-editor-lists-end.test.js | 27 ------- .../gutenberg-editor-lists.test.js | 47 ------------ .../gutenberg-editor-more.test.js | 16 ---- .../gutenberg-editor-separator.test.js | 16 ---- .../gutenberg-editor-spacer.test.js | 16 ---- .../gutenberg-editor-verse.test.js | 16 ---- packages/react-native-editor/package.json | 2 +- 18 files changed, 202 insertions(+), 268 deletions(-) create mode 100644 packages/block-library/src/latest-posts/test/__snapshots__/edit.native.js.snap create mode 100644 packages/block-library/src/latest-posts/test/edit.native.js create mode 100644 packages/block-library/src/more/test/__snapshots__/edit.native.js.snap create mode 100644 packages/block-library/src/more/test/edit.native.js create mode 100644 packages/block-library/src/separator/test/__snapshots__/edit.native.js.snap create mode 100644 packages/block-library/src/separator/test/edit.native.js create mode 100644 packages/block-library/src/verse/test/__snapshots__/edit.native.js.snap delete mode 100644 packages/react-native-editor/__device-tests__/gutenberg-editor-columns.test.js delete mode 100644 packages/react-native-editor/__device-tests__/gutenberg-editor-latest-posts.test.js delete mode 100644 packages/react-native-editor/__device-tests__/gutenberg-editor-lists-@canary.test.js delete mode 100644 packages/react-native-editor/__device-tests__/gutenberg-editor-lists-end.test.js delete mode 100644 packages/react-native-editor/__device-tests__/gutenberg-editor-lists.test.js delete mode 100644 packages/react-native-editor/__device-tests__/gutenberg-editor-more.test.js delete mode 100644 packages/react-native-editor/__device-tests__/gutenberg-editor-separator.test.js delete mode 100644 packages/react-native-editor/__device-tests__/gutenberg-editor-spacer.test.js delete mode 100644 packages/react-native-editor/__device-tests__/gutenberg-editor-verse.test.js diff --git a/packages/block-library/src/latest-posts/test/__snapshots__/edit.native.js.snap b/packages/block-library/src/latest-posts/test/__snapshots__/edit.native.js.snap new file mode 100644 index 00000000000000..dfdb88ce271d2c --- /dev/null +++ b/packages/block-library/src/latest-posts/test/__snapshots__/edit.native.js.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Latest Posts block inserts block 1`] = `""`; diff --git a/packages/block-library/src/latest-posts/test/edit.native.js b/packages/block-library/src/latest-posts/test/edit.native.js new file mode 100644 index 00000000000000..aa7397cdcd404d --- /dev/null +++ b/packages/block-library/src/latest-posts/test/edit.native.js @@ -0,0 +1,49 @@ +/** + * External dependencies + */ +import { + addBlock, + getEditorHtml, + initializeEditor, + getBlock, +} from 'test/helpers'; + +/** + * WordPress dependencies + */ +import { getBlockTypes, unregisterBlockType } from '@wordpress/blocks'; +import { registerCoreBlocks } from '@wordpress/block-library'; +import apiFetch from '@wordpress/api-fetch'; + +beforeAll( () => { + // Register all core blocks + registerCoreBlocks(); +} ); + +afterAll( () => { + // Clean up registered blocks + getBlockTypes().forEach( ( block ) => { + unregisterBlockType( block.name ); + } ); +} ); + +describe( 'Latest Posts block', () => { + afterEach( () => { + apiFetch.mockReset(); + } ); + + it( 'inserts block', async () => { + const screen = await initializeEditor(); + + // Mock return value for categories + apiFetch.mockReturnValueOnce( Promise.resolve( [] ) ); + + // Add block + await addBlock( screen, 'Latest Posts' ); + + // Get block + const latestPostsBlock = await getBlock( screen, 'Latest Posts' ); + expect( latestPostsBlock ).toBeVisible(); + expect( getEditorHtml() ).toMatchSnapshot(); + } ); +} ); diff --git a/packages/block-library/src/more/test/__snapshots__/edit.native.js.snap b/packages/block-library/src/more/test/__snapshots__/edit.native.js.snap new file mode 100644 index 00000000000000..0bc3714894afb7 --- /dev/null +++ b/packages/block-library/src/more/test/__snapshots__/edit.native.js.snap @@ -0,0 +1,7 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`More block inserts block 1`] = ` +" + +" +`; diff --git a/packages/block-library/src/more/test/edit.native.js b/packages/block-library/src/more/test/edit.native.js new file mode 100644 index 00000000000000..cc84c7f3fc2b79 --- /dev/null +++ b/packages/block-library/src/more/test/edit.native.js @@ -0,0 +1,41 @@ +/** + * External dependencies + */ +import { + addBlock, + getEditorHtml, + initializeEditor, + getBlock, +} from 'test/helpers'; + +/** + * WordPress dependencies + */ +import { getBlockTypes, unregisterBlockType } from '@wordpress/blocks'; +import { registerCoreBlocks } from '@wordpress/block-library'; + +beforeAll( () => { + // Register all core blocks + registerCoreBlocks(); +} ); + +afterAll( () => { + // Clean up registered blocks + getBlockTypes().forEach( ( block ) => { + unregisterBlockType( block.name ); + } ); +} ); + +describe( 'More block', () => { + it( 'inserts block', async () => { + const screen = await initializeEditor(); + + // Add block + await addBlock( screen, 'More' ); + + // Get block + const moreBlock = await getBlock( screen, 'More' ); + expect( moreBlock ).toBeVisible(); + expect( getEditorHtml() ).toMatchSnapshot(); + } ); +} ); diff --git a/packages/block-library/src/separator/test/__snapshots__/edit.native.js.snap b/packages/block-library/src/separator/test/__snapshots__/edit.native.js.snap new file mode 100644 index 00000000000000..5ab1ac00a0481f --- /dev/null +++ b/packages/block-library/src/separator/test/__snapshots__/edit.native.js.snap @@ -0,0 +1,7 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Separator block inserts block 1`] = ` +" +
+" +`; diff --git a/packages/block-library/src/separator/test/edit.native.js b/packages/block-library/src/separator/test/edit.native.js new file mode 100644 index 00000000000000..0346cb3b2e6bfc --- /dev/null +++ b/packages/block-library/src/separator/test/edit.native.js @@ -0,0 +1,41 @@ +/** + * External dependencies + */ +import { + addBlock, + getEditorHtml, + initializeEditor, + getBlock, +} from 'test/helpers'; + +/** + * WordPress dependencies + */ +import { getBlockTypes, unregisterBlockType } from '@wordpress/blocks'; +import { registerCoreBlocks } from '@wordpress/block-library'; + +beforeAll( () => { + // Register all core blocks + registerCoreBlocks(); +} ); + +afterAll( () => { + // Clean up registered blocks + getBlockTypes().forEach( ( block ) => { + unregisterBlockType( block.name ); + } ); +} ); + +describe( 'Separator block', () => { + it( 'inserts block', async () => { + const screen = await initializeEditor(); + + // Add block + await addBlock( screen, 'Separator' ); + + // Get block + const separatorBlock = await getBlock( screen, 'Separator' ); + expect( separatorBlock ).toBeVisible(); + expect( getEditorHtml() ).toMatchSnapshot(); + } ); +} ); diff --git a/packages/block-library/src/verse/test/__snapshots__/edit.native.js.snap b/packages/block-library/src/verse/test/__snapshots__/edit.native.js.snap new file mode 100644 index 00000000000000..b28fe2ddf67a53 --- /dev/null +++ b/packages/block-library/src/verse/test/__snapshots__/edit.native.js.snap @@ -0,0 +1,13 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Verse block inserts block 1`] = ` +" +

+"
+`;
+
+exports[`Verse block renders block text set as initial content 1`] = `
+"
+
Sample text
+" +`; diff --git a/packages/block-library/src/verse/test/edit.native.js b/packages/block-library/src/verse/test/edit.native.js index 99cc10bc44abec..96bc431be4bbd7 100644 --- a/packages/block-library/src/verse/test/edit.native.js +++ b/packages/block-library/src/verse/test/edit.native.js @@ -1,47 +1,54 @@ /** * External dependencies */ -import { render } from 'test/helpers'; - -/** - * Internal dependencies - */ -import { metadata, settings, name } from '../index'; +import { + addBlock, + getEditorHtml, + initializeEditor, + getBlock, +} from 'test/helpers'; /** * WordPress dependencies */ -import { BlockEdit } from '@wordpress/block-editor'; -import { registerBlockType, unregisterBlockType } from '@wordpress/blocks'; - -const Verse = ( { clientId, ...props } ) => ( - -); - -describe( 'Verse Block', () => { - beforeAll( () => { - registerBlockType( name, { - ...metadata, - ...settings, - } ); - } ); +import { getBlockTypes, unregisterBlockType } from '@wordpress/blocks'; +import { registerCoreBlocks } from '@wordpress/block-library'; + +beforeAll( () => { + // Register all core blocks + registerCoreBlocks(); +} ); - afterAll( () => { - unregisterBlockType( name ); +afterAll( () => { + // Clean up registered blocks + getBlockTypes().forEach( ( block ) => { + unregisterBlockType( block.name ); } ); +} ); + +describe( 'Verse block', () => { + it( 'inserts block', async () => { + const screen = await initializeEditor(); - it( 'renders without crashing', () => { - const component = render( ); - const rendered = component.toJSON(); - expect( rendered ).toBeTruthy(); + // Add block + await addBlock( screen, 'Verse' ); + + // Get block + const verseBlock = await getBlock( screen, 'Verse' ); + expect( verseBlock ).toBeVisible(); + expect( getEditorHtml() ).toMatchSnapshot(); } ); - it( 'renders given text without crashing', () => { - const component = render( - - ); - expect( - component.getByDisplayValue( '
sample text
' ) - ).toBeTruthy(); + it( 'renders block text set as initial content', async () => { + const screen = await initializeEditor( { + initialHtml: ` +
Sample text
+ `, + } ); + + // Get block + const verseBlock = await getBlock( screen, 'Verse' ); + expect( verseBlock ).toBeVisible(); + expect( getEditorHtml() ).toMatchSnapshot(); } ); } ); diff --git a/packages/react-native-editor/__device-tests__/gutenberg-editor-columns.test.js b/packages/react-native-editor/__device-tests__/gutenberg-editor-columns.test.js deleted file mode 100644 index b57ea2588d7ee6..00000000000000 --- a/packages/react-native-editor/__device-tests__/gutenberg-editor-columns.test.js +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Internal dependencies - */ -import { blockNames } from './pages/editor-page'; -import testData from './helpers/test-data'; - -describe( 'Gutenberg Editor Columns Block test', () => { - it( 'should be able to handle a columns width unit from web', async () => { - await editorPage.setHtmlContent( - testData.columnsWithDifferentUnitsHtml - ); - - const columnsBlock = await editorPage.getFirstBlockVisible(); - await columnsBlock.click(); - - expect( columnsBlock ).toBeTruthy(); - await editorPage.removeBlockAtPosition( blockNames.columns ); - } ); -} ); diff --git a/packages/react-native-editor/__device-tests__/gutenberg-editor-latest-posts.test.js b/packages/react-native-editor/__device-tests__/gutenberg-editor-latest-posts.test.js deleted file mode 100644 index a5b8ec8d1a9eda..00000000000000 --- a/packages/react-native-editor/__device-tests__/gutenberg-editor-latest-posts.test.js +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Internal dependencies - */ -import { blockNames } from './pages/editor-page'; - -describe( 'Gutenberg Editor Latest Post Block tests', () => { - it( 'should be able to add a Latests-Posts block', async () => { - await editorPage.addNewBlock( blockNames.latestPosts ); - const latestPostsBlock = await editorPage.getBlockAtPosition( - blockNames.latestPosts - ); - - expect( latestPostsBlock ).toBeTruthy(); - await editorPage.removeBlockAtPosition( blockNames.latestPosts ); - } ); -} ); diff --git a/packages/react-native-editor/__device-tests__/gutenberg-editor-lists-@canary.test.js b/packages/react-native-editor/__device-tests__/gutenberg-editor-lists-@canary.test.js deleted file mode 100644 index d65fa3e6227d22..00000000000000 --- a/packages/react-native-editor/__device-tests__/gutenberg-editor-lists-@canary.test.js +++ /dev/null @@ -1,61 +0,0 @@ -/** - * Internal dependencies - */ -import { blockNames } from './pages/editor-page'; -import { isAndroid } from './helpers/utils'; -import testData from './helpers/test-data'; - -describe( 'Gutenberg Editor tests for List block', () => { - it.skip( 'should be able to add a new List block', async () => { - await editorPage.addNewBlock( blockNames.list ); - let listBlockElement = await editorPage.getListBlockAtPosition( 1, { - isEmptyBlock: true, - } ); - - await editorPage.typeTextToTextBlock( - listBlockElement, - testData.listItem1, - false - ); - - listBlockElement = await editorPage.getListBlockAtPosition(); - - // Send an Enter. - await editorPage.typeTextToTextBlock( listBlockElement, '\n', false ); - - // Send the second list item text. - await editorPage.typeTextToTextBlock( - listBlockElement, - testData.listItem2, - false - ); - - // Switch to html and verify html. - const html = await editorPage.getHtmlContent(); - expect( html.toLowerCase() ).toBe( testData.listHtml.toLowerCase() ); - } ); - - // This test depends on being run immediately after 'should be able to add a new List block' - it.skip( 'should update format to ordered list, using toolbar button', async () => { - let listBlockElement = await editorPage.getListBlockAtPosition(); - - if ( isAndroid() ) { - await listBlockElement.click(); - } - - // Send a click on the order list format button. - await editorPage.clickOrderedListToolBarButton(); - - // Switch to html and verify html. - const html = await editorPage.getHtmlContent(); - expect( html.toLowerCase() ).toBe( - testData.listHtmlOrdered.toLowerCase() - ); - // Remove list block to return editor to empty state. - listBlockElement = await editorPage.getBlockAtPosition( - blockNames.list - ); - await listBlockElement.click(); - await editorPage.removeBlockAtPosition( blockNames.list ); - } ); -} ); diff --git a/packages/react-native-editor/__device-tests__/gutenberg-editor-lists-end.test.js b/packages/react-native-editor/__device-tests__/gutenberg-editor-lists-end.test.js deleted file mode 100644 index dcca24cf6badf1..00000000000000 --- a/packages/react-native-editor/__device-tests__/gutenberg-editor-lists-end.test.js +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Internal dependencies - */ -import { blockNames } from './pages/editor-page'; -import testData from './helpers/test-data'; - -describe( 'Gutenberg Editor tests for List block (end)', () => { - it.skip( 'should be able to end a List block', async () => { - await editorPage.addNewBlock( blockNames.list ); - const listBlockElement = await editorPage.getListBlockAtPosition(); - - await editorPage.typeTextToTextBlock( - listBlockElement, - testData.listItem1, - false - ); - - // Send an Enter. - await editorPage.typeTextToTextBlock( listBlockElement, '\n\n', false ); - - const html = await editorPage.getHtmlContent(); - - expect( html.toLowerCase() ).toBe( - testData.listEndedHtml.toLowerCase() - ); - } ); -} ); diff --git a/packages/react-native-editor/__device-tests__/gutenberg-editor-lists.test.js b/packages/react-native-editor/__device-tests__/gutenberg-editor-lists.test.js deleted file mode 100644 index 7c666ae40596e3..00000000000000 --- a/packages/react-native-editor/__device-tests__/gutenberg-editor-lists.test.js +++ /dev/null @@ -1,47 +0,0 @@ -/** - * Internal dependencies - */ -import { blockNames } from './pages/editor-page'; -import { waitIfAndroid, backspace } from './helpers/utils'; - -describe( 'Gutenberg Editor tests for List block', () => { - // Prevent regression of https://github.com/wordpress-mobile/gutenberg-mobile/issues/871 - it.skip( 'should handle spaces in a list', async () => { - await editorPage.addNewBlock( blockNames.list ); - let listBlockElement = await editorPage.getListBlockAtPosition(); - - // Send the list item text. - await editorPage.typeTextToTextBlock( listBlockElement, ' a', false ); - - // Send an Enter. - await editorPage.typeTextToTextBlock( listBlockElement, '\n', false ); - - // Instead of introducing separate conditions for local and CI environment, add this wait for Android to accommodate both environments - await waitIfAndroid(); - - // Send a backspace. - await editorPage.typeTextToTextBlock( - listBlockElement, - backspace, - false - ); - - // There is a delay in Sauce Labs when a key is sent - // There isn't an element to check as it's being typed into an element that already exists, workaround is to add this wait until there's a better solution - await waitIfAndroid(); - - // Switch to html and verify html. - const html = await editorPage.getHtmlContent(); - - expect( html.toLowerCase() ).toBe( - ` -
  • a
-` - ); - - // Remove list block to reset editor to clean state. - listBlockElement = await editorPage.getListBlockAtPosition(); - await listBlockElement.click(); - await editorPage.removeBlockAtPosition( blockNames.list ); - } ); -} ); diff --git a/packages/react-native-editor/__device-tests__/gutenberg-editor-more.test.js b/packages/react-native-editor/__device-tests__/gutenberg-editor-more.test.js deleted file mode 100644 index a98627aa5c801c..00000000000000 --- a/packages/react-native-editor/__device-tests__/gutenberg-editor-more.test.js +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Internal dependencies - */ -import { blockNames } from './pages/editor-page'; - -describe( 'Gutenberg Editor More Block test', () => { - it( 'should be able to add a more block', async () => { - await editorPage.addNewBlock( blockNames.more ); - const moreBlock = await editorPage.getBlockAtPosition( - blockNames.more - ); - - expect( moreBlock ).toBeTruthy(); - await editorPage.removeBlockAtPosition( blockNames.more ); - } ); -} ); diff --git a/packages/react-native-editor/__device-tests__/gutenberg-editor-separator.test.js b/packages/react-native-editor/__device-tests__/gutenberg-editor-separator.test.js deleted file mode 100644 index 08b2b5b4709d3e..00000000000000 --- a/packages/react-native-editor/__device-tests__/gutenberg-editor-separator.test.js +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Internal dependencies - */ -import { blockNames } from './pages/editor-page'; - -describe( 'Gutenberg Editor Separator Block test', () => { - it( 'should be able to add a separator block', async () => { - await editorPage.addNewBlock( blockNames.separator ); - const separatorBlock = await editorPage.getBlockAtPosition( - blockNames.separator - ); - - expect( separatorBlock ).toBeTruthy(); - await editorPage.removeBlockAtPosition( blockNames.separator ); - } ); -} ); diff --git a/packages/react-native-editor/__device-tests__/gutenberg-editor-spacer.test.js b/packages/react-native-editor/__device-tests__/gutenberg-editor-spacer.test.js deleted file mode 100644 index 721ae86ea7eee4..00000000000000 --- a/packages/react-native-editor/__device-tests__/gutenberg-editor-spacer.test.js +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Internal dependencies - */ -import { blockNames } from './pages/editor-page'; - -describe( 'Gutenberg Editor Spacer Block test', () => { - it( 'should be able to add a spacer block', async () => { - await editorPage.addNewBlock( blockNames.spacer ); - const spacerBlock = await editorPage.getBlockAtPosition( - blockNames.spacer - ); - - expect( spacerBlock ).toBeTruthy(); - await editorPage.removeBlockAtPosition( blockNames.spacer ); - } ); -} ); diff --git a/packages/react-native-editor/__device-tests__/gutenberg-editor-verse.test.js b/packages/react-native-editor/__device-tests__/gutenberg-editor-verse.test.js deleted file mode 100644 index 93068088f84896..00000000000000 --- a/packages/react-native-editor/__device-tests__/gutenberg-editor-verse.test.js +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Internal dependencies - */ -import { blockNames } from './pages/editor-page'; - -describe( 'Gutenberg Editor Verse Block Tests', () => { - it( 'should be able to add a verse block', async () => { - await editorPage.addNewBlock( blockNames.verse ); - const verseBlock = await editorPage.getBlockAtPosition( - blockNames.verse - ); - - expect( verseBlock ).toBeTruthy(); - await editorPage.removeBlockAtPosition( blockNames.verse ); - } ); -} ); diff --git a/packages/react-native-editor/package.json b/packages/react-native-editor/package.json index 0a4f901c8f1c49..1a8649b93f64d7 100644 --- a/packages/react-native-editor/package.json +++ b/packages/react-native-editor/package.json @@ -108,7 +108,7 @@ "test": "cross-env NODE_ENV=test jest --verbose --config ../../test/native/jest.config.js", "test:debug": "cross-env NODE_ENV=test node --inspect-brk ../../node_modules/.bin/jest --runInBand --verbose --config ../../test/native/jest.config.js", "test:update": "npm run test -- --updateSnapshot", - "device-tests": "cross-env NODE_ENV=test jest --forceExit --detectOpenHandles --no-cache --maxWorkers=3 --verbose --config ./jest_ui.config.js", + "device-tests": "cross-env NODE_ENV=test jest --forceExit --detectOpenHandles --no-cache --maxWorkers=3 --testPathIgnorePatterns=@canary --verbose --config ./jest_ui.config.js", "device-tests-canary": "cross-env NODE_ENV=test jest --forceExit --detectOpenHandles --no-cache --maxWorkers=2 --testPathPattern=@canary --verbose --config ./jest_ui.config.js", "device-tests:local": "cross-env NODE_ENV=test jest --runInBand --detectOpenHandles --verbose --forceExit --config ./jest_ui.config.js", "device-tests:debug": "cross-env NODE_ENV=test node $NODE_DEBUG_OPTION --inspect-brk node_modules/jest/bin/jest --runInBand --detectOpenHandles --verbose --config ./jest_ui.config.js", From 2e878227d7d732288cce7bd428b43991cd9a769b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9?= <583546+oandregal@users.noreply.github.com> Date: Fri, 16 Dec 2022 12:15:24 +0100 Subject: [PATCH 36/51] Bundle `WP_Theme_JSON` class instead of inheriting per WordPress version (#46579) --- ....php => class-wp-theme-json-gutenberg.php} | 3831 ++++++++++++----- .../wordpress-6.2/class-wp-theme-json-6-2.php | 391 -- .../class-wp-theme-json-gutenberg.php | 31 - lib/load.php | 4 +- 4 files changed, 2700 insertions(+), 1557 deletions(-) rename lib/{compat/wordpress-6.1/class-wp-theme-json-6-1.php => class-wp-theme-json-gutenberg.php} (53%) delete mode 100644 lib/compat/wordpress-6.2/class-wp-theme-json-6-2.php delete mode 100644 lib/experimental/class-wp-theme-json-gutenberg.php diff --git a/lib/compat/wordpress-6.1/class-wp-theme-json-6-1.php b/lib/class-wp-theme-json-gutenberg.php similarity index 53% rename from lib/compat/wordpress-6.1/class-wp-theme-json-6-1.php rename to lib/class-wp-theme-json-gutenberg.php index 0c0ec0451aec4e..001798e5a326ed 100644 --- a/lib/compat/wordpress-6.1/class-wp-theme-json-6-1.php +++ b/lib/class-wp-theme-json-gutenberg.php @@ -1,8 +1,9 @@ array( ':visited', ':hover', ':focus', ':active' ), - 'button' => array( ':visited', ':hover', ':focus', ':active' ), - ); + const ROOT_BLOCK_SELECTOR = 'body'; /** * The sources of data this object can represent. * + * @since 5.8.0 + * @since 6.1.0 Added 'blocks'. * @var string[] */ const VALID_ORIGINS = array( @@ -44,11 +59,140 @@ class WP_Theme_JSON_6_1 extends WP_Theme_JSON { 'custom', ); + /** + * Presets are a set of values that serve + * to bootstrap some styles: colors, font sizes, etc. + * + * They are a unkeyed array of values such as: + * + * ```php + * array( + * array( + * 'slug' => 'unique-name-within-the-set', + * 'name' => 'Name for the UI', + * => 'value' + * ), + * ) + * ``` + * + * This contains the necessary metadata to process them: + * + * - path => Where to find the preset within the settings section. + * - prevent_override => Disables override of default presets by theme presets. + * The relationship between whether to override the defaults + * and whether the defaults are enabled is inverse: + * - If defaults are enabled => theme presets should not be overriden + * - If defaults are disabled => theme presets should be overriden + * For example, a theme sets defaultPalette to false, + * making the default palette hidden from the user. + * In that case, we want all the theme presets to be present, + * so they should override the defaults by setting this false. + * - use_default_names => whether to use the default names + * - value_key => the key that represents the value + * - value_func => optionally, instead of value_key, a function to generate + * the value that takes a preset as an argument + * (either value_key or value_func should be present) + * - css_vars => template string to use in generating the CSS Custom Property. + * Example output: "--wp--preset--duotone--blue: " will generate as many CSS Custom Properties as presets defined + * substituting the $slug for the slug's value for each preset value. + * - classes => array containing a structure with the classes to + * generate for the presets, where for each array item + * the key is the class name and the value the property name. + * The "$slug" substring will be replaced by the slug of each preset. + * For example: + * 'classes' => array( + * '.has-$slug-color' => 'color', + * '.has-$slug-background-color' => 'background-color', + * '.has-$slug-border-color' => 'border-color', + * ) + * - properties => array of CSS properties to be used by kses to + * validate the content of each preset + * by means of the remove_insecure_properties method. + * + * @since 5.8.0 + * @since 5.9.0 Added the `color.duotone` and `typography.fontFamilies` presets, + * `use_default_names` preset key, and simplified the metadata structure. + * @since 6.0.0 Replaced `override` with `prevent_override` and updated the + * `prevent_override` value for `color.duotone` to use `color.defaultDuotone`. + * @var array + */ + const PRESETS_METADATA = array( + array( + 'path' => array( 'color', 'palette' ), + 'prevent_override' => array( 'color', 'defaultPalette' ), + 'use_default_names' => false, + 'value_key' => 'color', + 'css_vars' => '--wp--preset--color--$slug', + 'classes' => array( + '.has-$slug-color' => 'color', + '.has-$slug-background-color' => 'background-color', + '.has-$slug-border-color' => 'border-color', + ), + 'properties' => array( 'color', 'background-color', 'border-color' ), + ), + array( + 'path' => array( 'color', 'gradients' ), + 'prevent_override' => array( 'color', 'defaultGradients' ), + 'use_default_names' => false, + 'value_key' => 'gradient', + 'css_vars' => '--wp--preset--gradient--$slug', + 'classes' => array( '.has-$slug-gradient-background' => 'background' ), + 'properties' => array( 'background' ), + ), + array( + 'path' => array( 'color', 'duotone' ), + 'prevent_override' => array( 'color', 'defaultDuotone' ), + 'use_default_names' => false, + 'value_func' => 'gutenberg_get_duotone_filter_property', + 'css_vars' => '--wp--preset--duotone--$slug', + 'classes' => array(), + 'properties' => array( 'filter' ), + ), + array( + 'path' => array( 'typography', 'fontSizes' ), + 'prevent_override' => false, + 'use_default_names' => true, + 'value_func' => 'gutenberg_get_typography_font_size_value', + 'css_vars' => '--wp--preset--font-size--$slug', + 'classes' => array( '.has-$slug-font-size' => 'font-size' ), + 'properties' => array( 'font-size' ), + ), + array( + 'path' => array( 'typography', 'fontFamilies' ), + 'prevent_override' => false, + 'use_default_names' => false, + 'value_key' => 'fontFamily', + 'css_vars' => '--wp--preset--font-family--$slug', + 'classes' => array( '.has-$slug-font-family' => 'font-family' ), + 'properties' => array( 'font-family' ), + ), + array( + 'path' => array( 'spacing', 'spacingSizes' ), + 'prevent_override' => false, + 'use_default_names' => true, + 'value_key' => 'size', + 'css_vars' => '--wp--preset--spacing--$slug', + 'classes' => array(), + 'properties' => array( 'padding', 'margin' ), + ), + ); + /** * Metadata for style properties. * * Each element is a direct mapping from the CSS property name to the * path to the value in theme.json & block attributes. + * + * @since 5.8.0 + * @since 5.9.0 Added the `border-*`, `font-family`, `font-style`, `font-weight`, + * `letter-spacing`, `margin-*`, `padding-*`, `--wp--style--block-gap`, + * `text-decoration`, `text-transform`, and `filter` properties, + * simplified the metadata structure. + * @since 6.1.0 Added the `border-*-color`, `border-*-width`, `border-*-style`, + * `--wp--style--root--padding-*`, and `box-shadow` properties, + * removed the `--wp--style--block-gap` property. + * @since 6.2.0 Added `min-height`. + * @var array */ const PROPERTIES_METADATA = array( 'background' => array( 'color', 'gradient' ), @@ -85,6 +229,7 @@ class WP_Theme_JSON_6_1 extends WP_Theme_JSON { 'margin-right' => array( 'spacing', 'margin', 'right' ), 'margin-bottom' => array( 'spacing', 'margin', 'bottom' ), 'margin-left' => array( 'spacing', 'margin', 'left' ), + 'min-height' => array( 'dimensions', 'minHeight' ), 'outline-color' => array( 'outline', 'color' ), 'outline-offset' => array( 'outline', 'offset' ), 'outline-style' => array( 'outline', 'style' ), @@ -106,260 +251,136 @@ class WP_Theme_JSON_6_1 extends WP_Theme_JSON { ); /** - * The valid elements that can be found under styles. + * Protected style properties. * - * @var string[] + * These style properties are only rendered if a setting enables it + * via a value other than `null`. + * + * Each element maps the style property to the corresponding theme.json + * setting key. + * + * @since 5.9.0 */ - const ELEMENTS = array( - 'link' => 'a:where(:not(.wp-element-button))', // The where is needed to lower the specificity. - 'heading' => 'h1, h2, h3, h4, h5, h6', - 'h1' => 'h1', - 'h2' => 'h2', - 'h3' => 'h3', - 'h4' => 'h4', - 'h5' => 'h5', - 'h6' => 'h6', - 'button' => '.wp-element-button, .wp-block-button__link', // We have the .wp-block-button__link class so that this will target older buttons that have been serialized. - 'caption' => '.wp-element-caption, .wp-block-audio figcaption, .wp-block-embed figcaption, .wp-block-gallery figcaption, .wp-block-image figcaption, .wp-block-table figcaption, .wp-block-video figcaption', // The block classes are necessary to target older content that won't use the new class names. - 'cite' => 'cite', - ); - - const __EXPERIMENTAL_ELEMENT_CLASS_NAMES = array( - 'button' => 'wp-element-button', - 'caption' => 'wp-element-caption', - ); - - // List of block support features that can have their related styles - // generated under their own feature level selector rather than the block's. - const BLOCK_SUPPORT_FEATURE_LEVEL_SELECTORS = array( - '__experimentalBorder' => 'border', - 'color' => 'color', - 'spacing' => 'spacing', - 'typography' => 'typography', + const PROTECTED_PROPERTIES = array( + 'spacing.blockGap' => array( 'spacing', 'blockGap' ), ); /** - * Constructor. + * Indirect metadata for style properties that are not directly output. * - * @since 5.8.0 + * Each element is a direct mapping from a CSS property name to the + * path to the value in theme.json & block attributes. * - * @param array $theme_json A structure that follows the theme.json schema. - * @param string $origin Optional. What source of data this object represents. - * One of 'default', 'theme', or 'custom'. Default 'theme'. + * 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 */ - public function __construct( $theme_json = array(), $origin = 'theme' ) { - if ( ! in_array( $origin, static::VALID_ORIGINS, true ) ) { - $origin = 'theme'; - } - - $this->theme_json = WP_Theme_JSON_Schema::migrate( $theme_json ); - $registry = WP_Block_Type_Registry::get_instance(); - $valid_block_names = array_keys( $registry->get_all_registered() ); - $valid_element_names = array_keys( static::ELEMENTS ); - $theme_json = static::sanitize( $this->theme_json, $valid_block_names, $valid_element_names ); - $this->theme_json = static::maybe_opt_in_into_settings( $theme_json ); - - // Internally, presets are keyed by origin. - $nodes = static::get_setting_nodes( $this->theme_json ); - foreach ( $nodes as $node ) { - foreach ( static::PRESETS_METADATA as $preset_metadata ) { - $path = array_merge( $node['path'], $preset_metadata['path'] ); - $preset = _wp_array_get( $this->theme_json, $path, null ); - if ( null !== $preset ) { - // If the preset is not already keyed by origin. - if ( isset( $preset[0] ) || empty( $preset ) ) { - _wp_array_set( $this->theme_json, $path, array( $origin => $preset ) ); - } - } - } - } - } + const INDIRECT_PROPERTIES_METADATA = array( + 'gap' => array( 'spacing', 'blockGap' ), + 'column-gap' => array( 'spacing', 'blockGap', 'left' ), + 'row-gap' => array( 'spacing', 'blockGap', 'top' ), + ); /** - * Given an element name, returns a class name. + * The top-level keys a theme.json can have. * - * @param string $element The name of the element. - * - * @return string The name of the class. - * - * @since 6.1.0 + * @since 5.8.0 As `ALLOWED_TOP_LEVEL_KEYS`. + * @since 5.9.0 Renamed from `ALLOWED_TOP_LEVEL_KEYS` to `VALID_TOP_LEVEL_KEYS`, + * added the `customTemplates` and `templateParts` values. + * @var string[] */ - public static function get_element_class_name( $element ) { - return array_key_exists( $element, static::__EXPERIMENTAL_ELEMENT_CLASS_NAMES ) ? static::__EXPERIMENTAL_ELEMENT_CLASS_NAMES[ $element ] : ''; - } + const VALID_TOP_LEVEL_KEYS = array( + 'customTemplates', + 'patterns', + 'settings', + 'styles', + 'templateParts', + 'version', + 'title', + ); /** - * Sanitizes the input according to the schemas. - * - * @since 5.8.0 - * @since 5.9.0 Added the `$valid_block_names` and `$valid_element_name` parameters. + * The valid properties under the settings key. * - * @param array $input Structure to sanitize. - * @param array $valid_block_names List of valid block names. - * @param array $valid_element_names List of valid element names. - * @return array The sanitized output. + * @since 5.8.0 As `ALLOWED_SETTINGS`. + * @since 5.9.0 Renamed from `ALLOWED_SETTINGS` to `VALID_SETTINGS`, + * added new properties for `border`, `color`, `spacing`, + * and `typography`, and renamed others according to the new schema. + * @since 6.0.0 Added `color.defaultDuotone`. + * @since 6.1.0 Added `layout.definitions` and `useRootPaddingAwareAlignments`. + * @since 6.2.0 Added `dimensions.minHeight`. + * @var array */ - protected static function sanitize( $input, $valid_block_names, $valid_element_names ) { - - $output = array(); - - if ( ! is_array( $input ) ) { - return $output; - } - - // Preserve only the top most level keys. - $output = array_intersect_key( $input, array_flip( static::VALID_TOP_LEVEL_KEYS ) ); - - // Remove any rules that are annotated as "top" in VALID_STYLES constant. - // Some styles are only meant to be available at the top-level (e.g.: blockGap), - // hence, the schema for blocks & elements should not have them. - $styles_non_top_level = static::VALID_STYLES; - foreach ( array_keys( $styles_non_top_level ) as $section ) { - if ( array_key_exists( $section, $styles_non_top_level ) && is_array( $styles_non_top_level[ $section ] ) ) { - foreach ( array_keys( $styles_non_top_level[ $section ] ) as $prop ) { - if ( 'top' === $styles_non_top_level[ $section ][ $prop ] ) { - unset( $styles_non_top_level[ $section ][ $prop ] ); - } - } - } - } - - // Build the schema based on valid block & element names. - $schema = array(); - $schema_styles_elements = array(); - - // Set allowed element pseudo selectors based on per element allow list. - // Target data structure in schema: - // e.g. - // - top level elements: `$schema['styles']['elements']['link'][':hover']`. - // - block level elements: `$schema['styles']['blocks']['core/button']['elements']['link'][':hover']`. - foreach ( $valid_element_names as $element ) { - $schema_styles_elements[ $element ] = $styles_non_top_level; - - if ( array_key_exists( $element, static::VALID_ELEMENT_PSEUDO_SELECTORS ) ) { - foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] as $pseudo_selector ) { - $schema_styles_elements[ $element ][ $pseudo_selector ] = $styles_non_top_level; - } - } - } - - $schema_styles_blocks = array(); - $schema_settings_blocks = array(); - foreach ( $valid_block_names as $block ) { - $schema_settings_blocks[ $block ] = static::VALID_SETTINGS; - $schema_styles_blocks[ $block ] = $styles_non_top_level; - $schema_styles_blocks[ $block ]['elements'] = $schema_styles_elements; - } - - $schema['styles'] = static::VALID_STYLES; - $schema['styles']['blocks'] = $schema_styles_blocks; - $schema['styles']['elements'] = $schema_styles_elements; - $schema['settings'] = static::VALID_SETTINGS; - $schema['settings']['blocks'] = $schema_settings_blocks; - - // Remove anything that's not present in the schema. - foreach ( array( 'styles', 'settings' ) as $subtree ) { - if ( ! isset( $input[ $subtree ] ) ) { - continue; - } - - if ( ! is_array( $input[ $subtree ] ) ) { - unset( $output[ $subtree ] ); - continue; - } - - $result = static::remove_keys_not_in_schema( $input[ $subtree ], $schema[ $subtree ] ); - - if ( empty( $result ) ) { - unset( $output[ $subtree ] ); - } else { - $output[ $subtree ] = $result; - } - } - - return $output; - } - - /** - * Removes insecure data from theme.json. - * - * @since 5.9.0 - * - * @param array $theme_json Structure to sanitize. - * @return array Sanitized structure. - */ - public static function remove_insecure_properties( $theme_json ) { - $sanitized = array(); - - $theme_json = WP_Theme_JSON_Schema::migrate( $theme_json ); - - $valid_block_names = array_keys( static::get_blocks_metadata() ); - $valid_element_names = array_keys( static::ELEMENTS ); - - $theme_json = static::sanitize( $theme_json, $valid_block_names, $valid_element_names ); - - $blocks_metadata = static::get_blocks_metadata(); - $style_nodes = static::get_style_nodes( $theme_json, $blocks_metadata ); - - foreach ( $style_nodes as $metadata ) { - $input = _wp_array_get( $theme_json, $metadata['path'], array() ); - if ( empty( $input ) ) { - continue; - } - - $output = static::remove_insecure_styles( $input ); - - // Get a reference to element name from path. - // $metadata['path'] = array('styles','elements','link');. - $current_element = $metadata['path'][ count( $metadata['path'] ) - 1 ]; - - // $output is stripped of pseudo selectors. Readd and process them - // for insecure styles here. - if ( array_key_exists( $current_element, static::VALID_ELEMENT_PSEUDO_SELECTORS ) ) { - - foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ] as $pseudo_selector ) { - if ( isset( $input[ $pseudo_selector ] ) ) { - $output[ $pseudo_selector ] = static::remove_insecure_styles( $input[ $pseudo_selector ] ); - } - } - } - - if ( ! empty( $output ) ) { - _wp_array_set( $sanitized, $metadata['path'], $output ); - } - } - - $setting_nodes = static::get_setting_nodes( $theme_json ); - foreach ( $setting_nodes as $metadata ) { - $input = _wp_array_get( $theme_json, $metadata['path'], array() ); - if ( empty( $input ) ) { - continue; - } - - $output = static::remove_insecure_settings( $input ); - if ( ! empty( $output ) ) { - _wp_array_set( $sanitized, $metadata['path'], $output ); - } - } - - if ( empty( $sanitized['styles'] ) ) { - unset( $theme_json['styles'] ); - } else { - $theme_json['styles'] = $sanitized['styles']; - } - - if ( empty( $sanitized['settings'] ) ) { - unset( $theme_json['settings'] ); - } else { - $theme_json['settings'] = $sanitized['settings']; - } - - return $theme_json; - } + const VALID_SETTINGS = array( + 'appearanceTools' => null, + 'useRootPaddingAwareAlignments' => null, + 'border' => array( + 'color' => null, + 'radius' => null, + 'style' => null, + 'width' => null, + ), + 'color' => array( + 'background' => null, + 'custom' => null, + 'customDuotone' => null, + 'customGradient' => null, + 'defaultDuotone' => null, + 'defaultGradients' => null, + 'defaultPalette' => null, + 'duotone' => null, + 'gradients' => null, + 'link' => null, + 'palette' => null, + 'text' => null, + ), + 'custom' => null, + 'dimensions' => array( + 'minHeight' => null, + ), + 'layout' => array( + 'contentSize' => null, + 'definitions' => null, + 'wideSize' => null, + ), + 'spacing' => array( + 'customSpacingSize' => null, + 'spacingSizes' => null, + 'spacingScale' => null, + 'blockGap' => null, + 'margin' => null, + 'padding' => null, + 'units' => null, + ), + 'typography' => array( + 'fluid' => null, + 'customFontSize' => null, + 'dropCap' => null, + 'fontFamilies' => null, + 'fontSizes' => null, + 'fontStyle' => null, + 'fontWeight' => null, + 'letterSpacing' => null, + 'lineHeight' => null, + 'textDecoration' => null, + 'textTransform' => null, + ), + ); /** * The valid properties under the styles key. * + * @since 5.8.0 As `ALLOWED_STYLES`. + * @since 5.9.0 Renamed from `ALLOWED_STYLES` to `VALID_STYLES`, + * added new properties for `border`, `filter`, `spacing`, + * and `typography`. + * @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 `dimensions.minHeight`. * @var array */ const VALID_STYLES = array( @@ -378,7 +399,10 @@ public static function remove_insecure_properties( $theme_json ) { 'gradient' => null, 'text' => null, ), - 'shadow' => null, + 'css' => null, + 'dimensions' => array( + 'minHeight' => null, + ), 'filter' => array( 'duotone' => null, ), @@ -388,6 +412,7 @@ public static function remove_insecure_properties( $theme_json ) { 'style' => null, 'width' => null, ), + 'shadow' => null, 'spacing' => array( 'margin' => null, 'padding' => null, @@ -406,68 +431,367 @@ public static function remove_insecure_properties( $theme_json ) { ); /** - * Function that appends a sub-selector to a existing one. + * Defines which pseudo selectors are enabled for which elements. * - * Given the compounded $selector "h1, h2, h3" - * and the $to_append selector ".some-class" the result will be - * "h1.some-class, h2.some-class, h3.some-class". + * The order of the selectors should be: visited, hover, focus, active. + * This is to ensure that 'visited' has the lowest specificity + * and the other selectors can always overwrite it. * - * @since 5.8.0 - * @since 6.1.0 Added append position. + * See https://core.trac.wordpress.org/ticket/56928. + * Note: this will affect both top-level and block-level elements. * - * @param string $selector Original selector. - * @param string $to_append Selector to append. - * @param string $position A position sub-selector should be appended. Default: 'right'. - * @return string + * @since 6.1.0 */ - protected static function append_to_selector( $selector, $to_append, $position = 'right' ) { - $new_selectors = array(); - $selectors = explode( ',', $selector ); - foreach ( $selectors as $sel ) { - $new_selectors[] = 'right' === $position ? $sel . $to_append : $to_append . $sel; - } + const VALID_ELEMENT_PSEUDO_SELECTORS = array( + 'link' => array( ':visited', ':hover', ':focus', ':active' ), + 'button' => array( ':visited', ':hover', ':focus', ':active' ), + ); - return implode( ',', $new_selectors ); - } + /** + * The valid elements that can be found under styles. + * + * @since 5.8.0 + * @since 6.1.0 Added `heading`, `button`, and `caption` elements. + * @var string[] + */ + const ELEMENTS = array( + 'link' => 'a:where(:not(.wp-element-button))', // The `where` is needed to lower the specificity. + 'heading' => 'h1, h2, h3, h4, h5, h6', + 'h1' => 'h1', + 'h2' => 'h2', + 'h3' => 'h3', + 'h4' => 'h4', + 'h5' => 'h5', + 'h6' => 'h6', + // We have the .wp-block-button__link class so that this will target older buttons that have been serialized. + 'button' => '.wp-element-button, .wp-block-button__link', + // The block classes are necessary to target older content that won't use the new class names. + 'caption' => '.wp-element-caption, .wp-block-audio figcaption, .wp-block-embed figcaption, .wp-block-gallery figcaption, .wp-block-image figcaption, .wp-block-table figcaption, .wp-block-video figcaption', + 'cite' => 'cite', + ); + + const __EXPERIMENTAL_ELEMENT_CLASS_NAMES = array( + 'button' => 'wp-element-button', + 'caption' => 'wp-element-caption', + ); /** - * Returns the metadata for each block. + * List of block support features that can have their related styles + * generated under their own feature level selector rather than the block's. * - * Example: + * @since 6.1.0 + * @var string[] + */ + const BLOCK_SUPPORT_FEATURE_LEVEL_SELECTORS = array( + '__experimentalBorder' => 'border', + 'color' => 'color', + 'spacing' => 'spacing', + 'typography' => 'typography', + ); + + /** + * Returns a class name by an element name. * - * { - * 'core/paragraph': { - * 'selector': 'p', - * 'elements': { - * 'link' => 'link selector', - * 'etc' => 'element selector' - * } - * }, - * 'core/heading': { - * 'selector': 'h1', - * 'elements': {} - * }, - * 'core/image': { - * 'selector': '.wp-block-image', - * 'duotone': 'img', - * 'elements': {} - * } - * } + * @since 6.1.0 * - * @return array Block metadata. + * @param string $element The name of the element. + * @return string The name of the class. */ - protected static function get_blocks_metadata() { - if ( null !== static::$blocks_metadata ) { - return static::$blocks_metadata; + public static function get_element_class_name( $element ) { + $class_name = ''; + + // TODO: Replace array_key_exists() with isset() check once WordPress drops + // support for PHP 5.6. See https://core.trac.wordpress.org/ticket/57067. + if ( array_key_exists( $element, static::__EXPERIMENTAL_ELEMENT_CLASS_NAMES ) ) { + $class_name = static::__EXPERIMENTAL_ELEMENT_CLASS_NAMES[ $element ]; } - static::$blocks_metadata = array(); + return $class_name; + } - $registry = WP_Block_Type_Registry::get_instance(); - $blocks = $registry->get_all_registered(); - foreach ( $blocks as $block_name => $block_type ) { - if ( - isset( $block_type->supports['__experimentalSelector'] ) && + /** + * Options that settings.appearanceTools enables. + * + * @since 6.0.0 + * @var array + */ + const APPEARANCE_TOOLS_OPT_INS = array( + array( 'border', 'color' ), + array( 'border', 'radius' ), + array( 'border', 'style' ), + array( 'border', 'width' ), + array( 'color', 'link' ), + array( 'dimensions', 'minHeight' ), + array( 'spacing', 'blockGap' ), + array( 'spacing', 'margin' ), + array( 'spacing', 'padding' ), + array( 'typography', 'lineHeight' ), + ); + + /** + * The latest version of the schema in use. + * + * @since 5.8.0 + * @since 5.9.0 Changed value from 1 to 2. + * @var int + */ + const LATEST_SCHEMA = 2; + + /** + * Constructor. + * + * @since 5.8.0 + * + * @param array $theme_json A structure that follows the theme.json schema. + * @param string $origin Optional. What source of data this object represents. + * One of 'default', 'theme', or 'custom'. Default 'theme'. + */ + public function __construct( $theme_json = array(), $origin = 'theme' ) { + if ( ! in_array( $origin, static::VALID_ORIGINS, true ) ) { + $origin = 'theme'; + } + + $this->theme_json = WP_Theme_JSON_Schema::migrate( $theme_json ); + $registry = WP_Block_Type_Registry::get_instance(); + $valid_block_names = array_keys( $registry->get_all_registered() ); + $valid_element_names = array_keys( static::ELEMENTS ); + $theme_json = static::sanitize( $this->theme_json, $valid_block_names, $valid_element_names ); + $this->theme_json = static::maybe_opt_in_into_settings( $theme_json ); + + // Internally, presets are keyed by origin. + $nodes = static::get_setting_nodes( $this->theme_json ); + foreach ( $nodes as $node ) { + foreach ( static::PRESETS_METADATA as $preset_metadata ) { + $path = $node['path']; + foreach ( $preset_metadata['path'] as $subpath ) { + $path[] = $subpath; + } + $preset = _wp_array_get( $this->theme_json, $path, null ); + if ( null !== $preset ) { + // If the preset is not already keyed by origin. + if ( isset( $preset[0] ) || empty( $preset ) ) { + _wp_array_set( $this->theme_json, $path, array( $origin => $preset ) ); + } + } + } + } + } + + /** + * Enables some opt-in settings if theme declared support. + * + * @since 5.9.0 + * + * @param array $theme_json A theme.json structure to modify. + * @return array The modified theme.json structure. + */ + protected static function maybe_opt_in_into_settings( $theme_json ) { + $new_theme_json = $theme_json; + + if ( + isset( $new_theme_json['settings']['appearanceTools'] ) && + true === $new_theme_json['settings']['appearanceTools'] + ) { + static::do_opt_in_into_settings( $new_theme_json['settings'] ); + } + + if ( isset( $new_theme_json['settings']['blocks'] ) && is_array( $new_theme_json['settings']['blocks'] ) ) { + foreach ( $new_theme_json['settings']['blocks'] as &$block ) { + if ( isset( $block['appearanceTools'] ) && ( true === $block['appearanceTools'] ) ) { + static::do_opt_in_into_settings( $block ); + } + } + } + + return $new_theme_json; + } + + /** + * Enables some settings. + * + * @since 5.9.0 + * + * @param array $context The context to which the settings belong. + */ + protected static function do_opt_in_into_settings( &$context ) { + foreach ( static::APPEARANCE_TOOLS_OPT_INS as $path ) { + // Use "unset prop" as a marker instead of "null" because + // "null" can be a valid value for some props (e.g. blockGap). + if ( 'unset prop' === _wp_array_get( $context, $path, 'unset prop' ) ) { + _wp_array_set( $context, $path, true ); + } + } + + unset( $context['appearanceTools'] ); + } + + /** + * Sanitizes the input according to the schemas. + * + * @since 5.8.0 + * @since 5.9.0 Added the `$valid_block_names` and `$valid_element_name` parameters. + * + * @param array $input Structure to sanitize. + * @param array $valid_block_names List of valid block names. + * @param array $valid_element_names List of valid element names. + * @return array The sanitized output. + */ + protected static function sanitize( $input, $valid_block_names, $valid_element_names ) { + + $output = array(); + + if ( ! is_array( $input ) ) { + return $output; + } + + // Preserve only the top most level keys. + $output = array_intersect_key( $input, array_flip( static::VALID_TOP_LEVEL_KEYS ) ); + + /* + * Remove any rules that are annotated as "top" in VALID_STYLES constant. + * Some styles are only meant to be available at the top-level (e.g.: blockGap), + * hence, the schema for blocks & elements should not have them. + */ + $styles_non_top_level = static::VALID_STYLES; + foreach ( array_keys( $styles_non_top_level ) as $section ) { + // array_key_exists() needs to be used instead of isset() because the value can be null. + if ( array_key_exists( $section, $styles_non_top_level ) && is_array( $styles_non_top_level[ $section ] ) ) { + foreach ( array_keys( $styles_non_top_level[ $section ] ) as $prop ) { + if ( 'top' === $styles_non_top_level[ $section ][ $prop ] ) { + unset( $styles_non_top_level[ $section ][ $prop ] ); + } + } + } + } + + // Build the schema based on valid block & element names. + $schema = array(); + $schema_styles_elements = array(); + + /* + * Set allowed element pseudo selectors based on per element allow list. + * Target data structure in schema: + * e.g. + * - top level elements: `$schema['styles']['elements']['link'][':hover']`. + * - block level elements: `$schema['styles']['blocks']['core/button']['elements']['link'][':hover']`. + */ + foreach ( $valid_element_names as $element ) { + $schema_styles_elements[ $element ] = $styles_non_top_level; + + // TODO: Replace array_key_exists() with isset() check once WordPress drops + // support for PHP 5.6. See https://core.trac.wordpress.org/ticket/57067. + if ( array_key_exists( $element, static::VALID_ELEMENT_PSEUDO_SELECTORS ) ) { + foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] as $pseudo_selector ) { + $schema_styles_elements[ $element ][ $pseudo_selector ] = $styles_non_top_level; + } + } + } + + $schema_styles_blocks = array(); + $schema_settings_blocks = array(); + foreach ( $valid_block_names as $block ) { + $schema_settings_blocks[ $block ] = static::VALID_SETTINGS; + $schema_styles_blocks[ $block ] = $styles_non_top_level; + $schema_styles_blocks[ $block ]['elements'] = $schema_styles_elements; + } + + $schema['styles'] = static::VALID_STYLES; + $schema['styles']['blocks'] = $schema_styles_blocks; + $schema['styles']['elements'] = $schema_styles_elements; + $schema['settings'] = static::VALID_SETTINGS; + $schema['settings']['blocks'] = $schema_settings_blocks; + + // Remove anything that's not present in the schema. + foreach ( array( 'styles', 'settings' ) as $subtree ) { + if ( ! isset( $input[ $subtree ] ) ) { + continue; + } + + if ( ! is_array( $input[ $subtree ] ) ) { + unset( $output[ $subtree ] ); + continue; + } + + $result = static::remove_keys_not_in_schema( $input[ $subtree ], $schema[ $subtree ] ); + + if ( empty( $result ) ) { + unset( $output[ $subtree ] ); + } else { + $output[ $subtree ] = $result; + } + } + + return $output; + } + + /** + * Appends a sub-selector to an existing one. + * + * Given the compounded $selector "h1, h2, h3" + * and the $to_append selector ".some-class" the result will be + * "h1.some-class, h2.some-class, h3.some-class". + * + * @since 5.8.0 + * @since 6.1.0 Added append position. + * + * @param string $selector Original selector. + * @param string $to_append Selector to append. + * @param string $position A position sub-selector should be appended. Default 'right'. + * @return string The new selector. + */ + protected static function append_to_selector( $selector, $to_append, $position = 'right' ) { + $new_selectors = array(); + $selectors = explode( ',', $selector ); + foreach ( $selectors as $sel ) { + $new_selectors[] = 'right' === $position ? $sel . $to_append : $to_append . $sel; + } + return implode( ',', $new_selectors ); + } + + /** + * Returns the metadata for each block. + * + * Example: + * + * { + * 'core/paragraph': { + * 'selector': 'p', + * 'elements': { + * 'link' => 'link selector', + * 'etc' => 'element selector' + * } + * }, + * 'core/heading': { + * 'selector': 'h1', + * 'elements': {} + * }, + * 'core/image': { + * 'selector': '.wp-block-image', + * 'duotone': 'img', + * 'elements': {} + * } + * } + * + * @since 5.8.0 + * @since 5.9.0 Added `duotone` key with CSS selector. + * @since 6.1.0 Added `features` key with block support feature level selectors. + * + * @return array Block metadata. + */ + protected static function get_blocks_metadata() { + // NOTE: the compat/6.1 version of this method in Gutenberg did not have these changes. + $registry = WP_Block_Type_Registry::get_instance(); + $blocks = $registry->get_all_registered(); + + // Is there metadata for all currently registered blocks? + $blocks = array_diff_key( $blocks, static::$blocks_metadata ); + if ( empty( $blocks ) ) { + return static::$blocks_metadata; + } + + foreach ( $blocks as $block_name => $block_type ) { + if ( + isset( $block_type->supports['__experimentalSelector'] ) && is_string( $block_type->supports['__experimentalSelector'] ) ) { static::$blocks_metadata[ $block_name ]['selector'] = $block_type->supports['__experimentalSelector']; @@ -501,7 +825,7 @@ protected static function get_blocks_metadata() { static::$blocks_metadata[ $block_name ]['features'] = $features; } - // Assign defaults, then override those that the block sets by itself. + // Assign defaults, then overwrite those that the block sets by itself. // If the block selector is compounded, will append the element to each // individual block selector. $block_selectors = explode( ',', static::$blocks_metadata[ $block_name ]['selector'] ); @@ -517,174 +841,90 @@ protected static function get_blocks_metadata() { static::$blocks_metadata[ $block_name ]['elements'][ $el_name ] = implode( ',', $element_selector ); } } + return static::$blocks_metadata; } /** - * Builds metadata for the style nodes, which returns in the form of: + * Given a tree, removes the keys that are not present in the schema. * - * [ - * [ - * 'path' => [ 'path', 'to', 'some', 'node' ], - * 'selector' => 'CSS selector for some node', - * 'duotone' => 'CSS selector for duotone for some node' - * ], - * [ - * 'path' => ['path', 'to', 'other', 'node' ], - * 'selector' => 'CSS selector for other node', - * 'duotone' => null - * ], - * ] + * It is recursive and modifies the input in-place. * * @since 5.8.0 * - * @param array $theme_json The tree to extract style nodes from. - * @param array $selectors List of selectors per block. - * @return array + * @param array $tree Input to process. + * @param array $schema Schema to adhere to. + * @return array The modified $tree. */ - protected static function get_style_nodes( $theme_json, $selectors = array() ) { - $nodes = array(); - if ( ! isset( $theme_json['styles'] ) ) { - return $nodes; - } + protected static function remove_keys_not_in_schema( $tree, $schema ) { + $tree = array_intersect_key( $tree, $schema ); - // Top-level. - $nodes[] = array( - 'path' => array( 'styles' ), - 'selector' => static::ROOT_BLOCK_SELECTOR, - ); + foreach ( $schema as $key => $data ) { + if ( ! isset( $tree[ $key ] ) ) { + continue; + } - if ( isset( $theme_json['styles']['elements'] ) ) { - foreach ( self::ELEMENTS as $element => $selector ) { - if ( ! isset( $theme_json['styles']['elements'][ $element ] ) || ! array_key_exists( $element, static::ELEMENTS ) ) { - continue; + if ( is_array( $schema[ $key ] ) && is_array( $tree[ $key ] ) ) { + $tree[ $key ] = static::remove_keys_not_in_schema( $tree[ $key ], $schema[ $key ] ); + + if ( empty( $tree[ $key ] ) ) { + unset( $tree[ $key ] ); } + } elseif ( is_array( $schema[ $key ] ) && ! is_array( $tree[ $key ] ) ) { + unset( $tree[ $key ] ); + } + } - // Handle element defaults. - $nodes[] = array( - 'path' => array( 'styles', 'elements', $element ), - 'selector' => static::ELEMENTS[ $element ], - ); - - // Handle any pseudo selectors for the element. - if ( array_key_exists( $element, static::VALID_ELEMENT_PSEUDO_SELECTORS ) ) { - foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] as $pseudo_selector ) { - - if ( isset( $theme_json['styles']['elements'][ $element ][ $pseudo_selector ] ) ) { - - $nodes[] = array( - 'path' => array( 'styles', 'elements', $element ), - 'selector' => static::append_to_selector( static::ELEMENTS[ $element ], $pseudo_selector ), - ); - } - } - } - } - } - - // Blocks. - if ( ! isset( $theme_json['styles']['blocks'] ) ) { - return $nodes; - } - - $nodes = array_merge( $nodes, static::get_block_nodes( $theme_json, $selectors ) ); - - // This filter allows us to modify the output of WP_Theme_JSON so that we can do things like loading block CSS independently. - return apply_filters( 'wp_theme_json_get_style_nodes', $nodes ); + return $tree; } /** - * A public helper to get the block nodes from a theme.json file. + * Returns the existing settings for each block. * - * @return array The block nodes in theme.json. - */ - public function get_styles_block_nodes() { - return static::get_block_nodes( $this->theme_json ); - } - - /** - * An internal method to get the block nodes from a theme.json file. + * Example: * - * @param array $theme_json The theme.json converted to an array. - * @param array $selectors Optional list of selectors per block. + * { + * 'root': { + * 'color': { + * 'custom': true + * } + * }, + * 'core/paragraph': { + * 'spacing': { + * 'customPadding': true + * } + * } + * } * - * @return array The block nodes in theme.json. + * @since 5.8.0 + * + * @return array Settings per block. */ - private static function get_block_nodes( $theme_json, $selectors = array() ) { - $selectors = empty( $selectors ) ? static::get_blocks_metadata() : $selectors; - $nodes = array(); - if ( ! isset( $theme_json['styles'] ) ) { - return $nodes; - } - - // Blocks. - if ( ! isset( $theme_json['styles']['blocks'] ) ) { - return $nodes; - } - - foreach ( $theme_json['styles']['blocks'] as $name => $node ) { - $selector = null; - if ( isset( $selectors[ $name ]['selector'] ) ) { - $selector = $selectors[ $name ]['selector']; - } - - $duotone_selector = null; - if ( isset( $selectors[ $name ]['duotone'] ) ) { - $duotone_selector = $selectors[ $name ]['duotone']; - } - - $feature_selectors = null; - if ( isset( $selectors[ $name ]['features'] ) ) { - $feature_selectors = $selectors[ $name ]['features']; - } - - $nodes[] = array( - 'name' => $name, - 'path' => array( 'styles', 'blocks', $name ), - 'selector' => $selector, - 'duotone' => $duotone_selector, - 'features' => $feature_selectors, - ); - - if ( isset( $theme_json['styles']['blocks'][ $name ]['elements'] ) ) { - foreach ( $theme_json['styles']['blocks'][ $name ]['elements'] as $element => $node ) { - $nodes[] = array( - 'path' => array( 'styles', 'blocks', $name, 'elements', $element ), - 'selector' => $selectors[ $name ]['elements'][ $element ], - ); - - // Handle any pseudo selectors for the element. - if ( array_key_exists( $element, static::VALID_ELEMENT_PSEUDO_SELECTORS ) ) { - foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] as $pseudo_selector ) { - if ( isset( $theme_json['styles']['blocks'][ $name ]['elements'][ $element ][ $pseudo_selector ] ) ) { - - $nodes[] = array( - 'path' => array( 'styles', 'blocks', $name, 'elements', $element ), - 'selector' => static::append_to_selector( $selectors[ $name ]['elements'][ $element ], $pseudo_selector ), - ); - } - } - } - } - } + public function get_settings() { + if ( ! isset( $this->theme_json['settings'] ) ) { + return array(); + } else { + return $this->theme_json['settings']; } - - return $nodes; } /** * 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. + * @since 5.8.0 + * @since 5.9.0 Removed the `$type` parameter`, added the `$types` and `$origins` parameters. + * + * @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. + * @return string The resulting stylesheet. */ public function get_stylesheet( $types = array( 'variables', 'styles', 'presets' ), $origins = null, $options = array() ) { if ( null === $origins ) { @@ -693,7 +933,7 @@ public function get_stylesheet( $types = array( 'variables', 'styles', 'presets' if ( is_string( $types ) ) { // Dispatch error and map old arguments to new ones. - _deprecated_argument( __FUNCTION__, '5.9' ); + _deprecated_argument( __FUNCTION__, '5.9.0' ); if ( 'block_styles' === $types ) { $types = array( 'styles', 'presets' ); } elseif ( 'css_variables' === $types ) { @@ -772,182 +1012,1306 @@ public function get_stylesheet( $types = array( 'variables', 'styles', 'presets' $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; } - /** - * Returns a filtered declarations array if there is a separator block with only a background - * style defined in theme.json by adding a color attribute to reflect the changes in the front. + * Returns the page templates of the active theme. * - * @param array $declarations List of declarations. + * @since 5.9.0 * - * @return array $declarations List of declarations filtered. + * @return array */ - private static function update_separator_declarations( $declarations ) { - $background_matches = array_values( - array_filter( - $declarations, - function( $declaration ) { - return 'background-color' === $declaration['name']; - } - ) - ); - if ( ! empty( $background_matches && isset( $background_matches[0]['value'] ) ) ) { - $border_color_matches = array_values( - array_filter( - $declarations, - function( $declaration ) { - return 'border-color' === $declaration['name']; - } - ) - ); - $text_color_matches = array_values( - array_filter( - $declarations, - function( $declaration ) { - return 'color' === $declaration['name']; - } - ) - ); - if ( empty( $border_color_matches ) && empty( $text_color_matches ) ) { - $declarations[] = array( - 'name' => 'color', - 'value' => $background_matches[0]['value'], + public function get_custom_templates() { + $custom_templates = array(); + if ( ! isset( $this->theme_json['customTemplates'] ) || ! is_array( $this->theme_json['customTemplates'] ) ) { + return $custom_templates; + } + + foreach ( $this->theme_json['customTemplates'] as $item ) { + if ( isset( $item['name'] ) ) { + $custom_templates[ $item['name'] ] = array( + 'title' => isset( $item['title'] ) ? $item['title'] : '', + 'postTypes' => isset( $item['postTypes'] ) ? $item['postTypes'] : array( 'page' ), ); } } - - return $declarations; + return $custom_templates; } /** - * Gets the CSS rules for a particular block from theme.json. + * Returns the template part data of active theme. * - * @param array $block_metadata Metadata about the block to get styles for. + * @since 5.9.0 * - * @return string Styles for the block. + * @return array */ - public function get_styles_for_block( $block_metadata ) { - $node = _wp_array_get( $this->theme_json, $block_metadata['path'], array() ); - $use_root_padding = isset( $this->theme_json['settings']['useRootPaddingAwareAlignments'] ) && true === $this->theme_json['settings']['useRootPaddingAwareAlignments']; - $selector = $block_metadata['selector']; - $settings = _wp_array_get( $this->theme_json, array( 'settings' ) ); - - // Process style declarations for block support features the current - // block contains selectors for. Values for a feature with a custom - // selector are filtered from the theme.json node before it is - // processed as normal. - $feature_declarations = array(); - - if ( ! empty( $block_metadata['features'] ) ) { - foreach ( $block_metadata['features'] as $feature_name => $feature_selector ) { - if ( ! empty( $node[ $feature_name ] ) ) { - // Create temporary node containing only the feature data - // to leverage existing `compute_style_properties` function. - $feature = array( $feature_name => $node[ $feature_name ] ); - // Generate the feature's declarations only. - $new_feature_declarations = static::compute_style_properties( $feature, $settings, null, $this->theme_json ); - - // Merge new declarations with any that already exist for - // the feature selector. This may occur when multiple block - // support features use the same custom selector. - if ( isset( $feature_declarations[ $feature_selector ] ) ) { - $feature_declarations[ $feature_selector ] = array_merge( $feature_declarations[ $feature_selector ], $new_feature_declarations ); - } else { - $feature_declarations[ $feature_selector ] = $new_feature_declarations; - } - - // Remove the feature from the block's node now the - // styles will be included under the feature level selector. - unset( $node[ $feature_name ] ); - } - } + public function get_template_parts() { + $template_parts = array(); + if ( ! isset( $this->theme_json['templateParts'] ) || ! is_array( $this->theme_json['templateParts'] ) ) { + return $template_parts; } - // Get a reference to element name from path. - // $block_metadata['path'] = array('styles','elements','link'); - // Make sure that $block_metadata['path'] describes an element node, like ['styles', 'element', 'link']. - // Skip non-element paths like just ['styles']. - $is_processing_element = in_array( 'elements', $block_metadata['path'], true ); - - $current_element = $is_processing_element ? $block_metadata['path'][ count( $block_metadata['path'] ) - 1 ] : null; - - $element_pseudo_allowed = array_key_exists( $current_element, static::VALID_ELEMENT_PSEUDO_SELECTORS ) ? static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ] : array(); - - // Check for allowed pseudo classes (e.g. ":hover") from the $selector ("a:hover"). - // This also resets the array keys. - $pseudo_matches = array_values( - array_filter( - $element_pseudo_allowed, - function( $pseudo_selector ) use ( $selector ) { - return str_contains( $selector, $pseudo_selector ); - } - ) - ); - - $pseudo_selector = isset( $pseudo_matches[0] ) ? $pseudo_matches[0] : null; - - // If the current selector is a pseudo selector that's defined in the allow list for the current - // element then compute the style properties for it. - // Otherwise just compute the styles for the default selector as normal. - if ( $pseudo_selector && isset( $node[ $pseudo_selector ] ) && array_key_exists( $current_element, static::VALID_ELEMENT_PSEUDO_SELECTORS ) && in_array( $pseudo_selector, static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ], true ) ) { - $declarations = static::compute_style_properties( $node[ $pseudo_selector ], $settings, null, $this->theme_json, $selector, $use_root_padding ); - } else { - $declarations = static::compute_style_properties( $node, $settings, null, $this->theme_json, $selector, $use_root_padding ); + foreach ( $this->theme_json['templateParts'] as $item ) { + if ( isset( $item['name'] ) ) { + $template_parts[ $item['name'] ] = array( + 'title' => isset( $item['title'] ) ? $item['title'] : '', + 'area' => isset( $item['area'] ) ? $item['area'] : '', + ); + } } + return $template_parts; + } + /** + * Converts each style section into a list of rulesets + * containing the block styles to be appended to the stylesheet. + * + * See glossary at https://developer.mozilla.org/en-US/docs/Web/CSS/Syntax + * + * For each section this creates a new ruleset such as: + * + * block-selector { + * style-property-one: value; + * } + * + * @since 5.8.0 As `get_block_styles()`. + * @since 5.9.0 Renamed from `get_block_styles()` to `get_block_classes()` + * and no longer returns preset classes. + * Removed the `$setting_nodes` parameter. + * @since 6.1.0 Moved most internal logic to `get_styles_for_block()`. + * + * @param array $style_nodes Nodes with styles. + * @return string The new stylesheet. + */ + protected function get_block_classes( $style_nodes ) { $block_rules = ''; - // 1. Separate the ones who use the general selector - // and the ones who use the duotone selector. - $declarations_duotone = array(); - foreach ( $declarations as $index => $declaration ) { - if ( 'filter' === $declaration['name'] ) { - unset( $declarations[ $index ] ); - $declarations_duotone[] = $declaration; + foreach ( $style_nodes as $metadata ) { + if ( null === $metadata['selector'] ) { + continue; } - } - - // Update declarations if there are separators with only background color defined. - if ( '.wp-block-separator' === $selector ) { - $declarations = static::update_separator_declarations( $declarations ); - } - - // 2. Generate and append the rules that use the general selector. - $block_rules .= static::to_ruleset( $selector, $declarations ); - - // 3. Generate and append the rules that use the duotone selector. - if ( isset( $block_metadata['duotone'] ) && ! empty( $declarations_duotone ) ) { - $selector_duotone = static::scope_selector( $block_metadata['selector'], $block_metadata['duotone'] ); - $block_rules .= static::to_ruleset( $selector_duotone, $declarations_duotone ); - } - - // 4. Generate Layout block gap styles. - if ( - static::ROOT_BLOCK_SELECTOR !== $selector && - ! empty( $block_metadata['name'] ) - ) { - $block_rules .= $this->get_layout_styles( $block_metadata ); - } - - // 5. Generate and append the feature level rulesets. - foreach ( $feature_declarations as $feature_selector => $individual_feature_declarations ) { - $block_rules .= static::to_ruleset( $feature_selector, $individual_feature_declarations ); + $block_rules .= static::get_styles_for_block( $metadata ); } return $block_rules; } /** - * Outputs the CSS for layout rules on the root. + * Gets the CSS layout rules for a particular block from theme.json layout definitions. * - * @param string $selector The root node selector. - * @param array $block_metadata The metadata for the root block. - * @return string The additional root rules CSS. + * @since 6.1.0 + * + * @param array $block_metadata Metadata about the block to get styles for. + * @return string Layout styles for the block. */ - public function get_root_layout_rules( $selector, $block_metadata ) { - $css = ''; - $settings = _wp_array_get( $this->theme_json, array( 'settings' ) ); + protected function get_layout_styles( $block_metadata ) { + $block_rules = ''; + $block_type = null; + + // Skip outputting layout styles if explicitly disabled. + if ( current_theme_supports( 'disable-layout-styles' ) ) { + return $block_rules; + } + + if ( isset( $block_metadata['name'] ) ) { + $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block_metadata['name'] ); + if ( ! block_has_support( $block_type, array( '__experimentalLayout' ), false ) ) { + return $block_rules; + } + } + + $selector = isset( $block_metadata['selector'] ) ? $block_metadata['selector'] : ''; + $has_block_gap_support = _wp_array_get( $this->theme_json, array( 'settings', 'spacing', 'blockGap' ) ) !== null; + $has_fallback_gap_support = ! $has_block_gap_support; // This setting isn't useful yet: it exists as a placeholder for a future explicit fallback gap styles support. + $node = _wp_array_get( $this->theme_json, $block_metadata['path'], array() ); + $layout_definitions = _wp_array_get( $this->theme_json, array( 'settings', 'layout', 'definitions' ), array() ); + $layout_selector_pattern = '/^[a-zA-Z0-9\-\.\ *+>:\(\)]*$/'; // Allow alphanumeric classnames, spaces, wildcard, sibling, child combinator and pseudo class selectors. + + // Gap styles will only be output if the theme has block gap support, or supports a fallback gap. + // Default layout gap styles will be skipped for themes that do not explicitly opt-in to blockGap with a `true` or `false` value. + if ( $has_block_gap_support || $has_fallback_gap_support ) { + $block_gap_value = null; + // Use a fallback gap value if block gap support is not available. + if ( ! $has_block_gap_support ) { + $block_gap_value = static::ROOT_BLOCK_SELECTOR === $selector ? '0.5em' : null; + if ( ! empty( $block_type ) ) { + $block_gap_value = _wp_array_get( $block_type->supports, array( 'spacing', 'blockGap', '__experimentalDefault' ), null ); + } + } else { + $block_gap_value = static::get_property_value( $node, array( 'spacing', 'blockGap' ) ); + } + + // Support split row / column values and concatenate to a shorthand value. + if ( is_array( $block_gap_value ) ) { + if ( isset( $block_gap_value['top'] ) && isset( $block_gap_value['left'] ) ) { + $gap_row = static::get_property_value( $node, array( 'spacing', 'blockGap', 'top' ) ); + $gap_column = static::get_property_value( $node, array( 'spacing', 'blockGap', 'left' ) ); + $block_gap_value = $gap_row === $gap_column ? $gap_row : $gap_row . ' ' . $gap_column; + } else { + // Skip outputting gap value if not all sides are provided. + $block_gap_value = null; + } + } + + // If the block should have custom gap, add the gap styles. + if ( null !== $block_gap_value && false !== $block_gap_value && '' !== $block_gap_value ) { + foreach ( $layout_definitions as $layout_definition_key => $layout_definition ) { + // Allow outputting fallback gap styles for flex layout type when block gap support isn't available. + if ( ! $has_block_gap_support && 'flex' !== $layout_definition_key ) { + continue; + } + + $class_name = sanitize_title( _wp_array_get( $layout_definition, array( 'className' ), false ) ); + $spacing_rules = _wp_array_get( $layout_definition, array( 'spacingStyles' ), array() ); + + if ( + ! empty( $class_name ) && + ! empty( $spacing_rules ) + ) { + foreach ( $spacing_rules as $spacing_rule ) { + $declarations = array(); + if ( + isset( $spacing_rule['selector'] ) && + preg_match( $layout_selector_pattern, $spacing_rule['selector'] ) && + ! empty( $spacing_rule['rules'] ) + ) { + // Iterate over each of the styling rules and substitute non-string values such as `null` with the real `blockGap` value. + foreach ( $spacing_rule['rules'] as $css_property => $css_value ) { + $current_css_value = is_string( $css_value ) ? $css_value : $block_gap_value; + if ( static::is_safe_css_declaration( $css_property, $current_css_value ) ) { + $declarations[] = array( + 'name' => $css_property, + 'value' => $current_css_value, + ); + } + } + + if ( ! $has_block_gap_support ) { + // For fallback gap styles, use lower specificity, to ensure styles do not unintentionally override theme styles. + $format = static::ROOT_BLOCK_SELECTOR === $selector ? ':where(.%2$s%3$s)' : ':where(%1$s.%2$s%3$s)'; + $layout_selector = sprintf( + $format, + $selector, + $class_name, + $spacing_rule['selector'] + ); + } else { + $format = static::ROOT_BLOCK_SELECTOR === $selector ? '%s .%s%s' : '%s.%s%s'; + $layout_selector = sprintf( + $format, + $selector, + $class_name, + $spacing_rule['selector'] + ); + } + $block_rules .= static::to_ruleset( $layout_selector, $declarations ); + } + } + } + } + } + } + + // Output base styles. + if ( + static::ROOT_BLOCK_SELECTOR === $selector + ) { + $valid_display_modes = array( 'block', 'flex', 'grid' ); + foreach ( $layout_definitions as $layout_definition ) { + $class_name = sanitize_title( _wp_array_get( $layout_definition, array( 'className' ), false ) ); + $base_style_rules = _wp_array_get( $layout_definition, array( 'baseStyles' ), array() ); + + if ( + ! empty( $class_name ) && + ! empty( $base_style_rules ) + ) { + // Output display mode. This requires special handling as `display` is not exposed in `safe_style_css_filter`. + if ( + ! empty( $layout_definition['displayMode'] ) && + is_string( $layout_definition['displayMode'] ) && + in_array( $layout_definition['displayMode'], $valid_display_modes, true ) + ) { + $layout_selector = sprintf( + '%s .%s', + $selector, + $class_name + ); + $block_rules .= static::to_ruleset( + $layout_selector, + array( + array( + 'name' => 'display', + 'value' => $layout_definition['displayMode'], + ), + ) + ); + } + + foreach ( $base_style_rules as $base_style_rule ) { + $declarations = array(); + + if ( + isset( $base_style_rule['selector'] ) && + preg_match( $layout_selector_pattern, $base_style_rule['selector'] ) && + ! empty( $base_style_rule['rules'] ) + ) { + foreach ( $base_style_rule['rules'] as $css_property => $css_value ) { + if ( static::is_safe_css_declaration( $css_property, $css_value ) ) { + $declarations[] = array( + 'name' => $css_property, + 'value' => $css_value, + ); + } + } + + $layout_selector = sprintf( + '%s .%s%s', + $selector, + $class_name, + $base_style_rule['selector'] + ); + $block_rules .= static::to_ruleset( $layout_selector, $declarations ); + } + } + } + } + } + return $block_rules; + } + + /** + * Creates new rulesets as classes for each preset value such as: + * + * .has-value-color { + * color: value; + * } + * + * .has-value-background-color { + * background-color: value; + * } + * + * .has-value-font-size { + * font-size: value; + * } + * + * .has-value-gradient-background { + * background: value; + * } + * + * p.has-value-gradient-background { + * background: value; + * } + * + * @since 5.9.0 + * + * @param array $setting_nodes Nodes with settings. + * @param array $origins List of origins to process presets from. + * @return string The new stylesheet. + */ + protected function get_preset_classes( $setting_nodes, $origins ) { + $preset_rules = ''; + + foreach ( $setting_nodes as $metadata ) { + if ( null === $metadata['selector'] ) { + continue; + } + + $selector = $metadata['selector']; + $node = _wp_array_get( $this->theme_json, $metadata['path'], array() ); + $preset_rules .= static::compute_preset_classes( $node, $selector, $origins ); + } + + return $preset_rules; + } + + /** + * Converts each styles section into a list of rulesets + * to be appended to the stylesheet. + * These rulesets contain all the css variables (custom variables and preset variables). + * + * See glossary at https://developer.mozilla.org/en-US/docs/Web/CSS/Syntax + * + * For each section this creates a new ruleset such as: + * + * block-selector { + * --wp--preset--category--slug: value; + * --wp--custom--variable: value; + * } + * + * @since 5.8.0 + * @since 5.9.0 Added the `$origins` parameter. + * + * @param array $nodes Nodes with settings. + * @param array $origins List of origins to process. + * @return string The new stylesheet. + */ + protected function get_css_variables( $nodes, $origins ) { + $stylesheet = ''; + foreach ( $nodes as $metadata ) { + if ( null === $metadata['selector'] ) { + continue; + } + + $selector = $metadata['selector']; + + $node = _wp_array_get( $this->theme_json, $metadata['path'], array() ); + $declarations = static::compute_preset_vars( $node, $origins ); + $theme_vars_declarations = static::compute_theme_vars( $node ); + foreach ( $theme_vars_declarations as $theme_vars_declaration ) { + $declarations[] = $theme_vars_declaration; + } + + $stylesheet .= static::to_ruleset( $selector, $declarations ); + } + + return $stylesheet; + } + + /** + * Given a selector and a declaration list, + * creates the corresponding ruleset. + * + * @since 5.8.0 + * + * @param string $selector CSS selector. + * @param array $declarations List of declarations. + * @return string The resulting CSS ruleset. + */ + protected static function to_ruleset( $selector, $declarations ) { + if ( empty( $declarations ) ) { + return ''; + } + + $declaration_block = array_reduce( + $declarations, + static function ( $carry, $element ) { + return $carry .= $element['name'] . ': ' . $element['value'] . ';'; }, + '' + ); + + return $selector . '{' . $declaration_block . '}'; + } + + /** + * Given a settings array, returns the generated rulesets + * for the preset classes. + * + * @since 5.8.0 + * @since 5.9.0 Added the `$origins` parameter. + * + * @param array $settings Settings to process. + * @param string $selector Selector wrapping the classes. + * @param array $origins List of origins to process. + * @return string The result of processing the presets. + */ + protected static function compute_preset_classes( $settings, $selector, $origins ) { + if ( static::ROOT_BLOCK_SELECTOR === $selector ) { + // Classes at the global level do not need any CSS prefixed, + // and we don't want to increase its specificity. + $selector = ''; + } + + $stylesheet = ''; + foreach ( static::PRESETS_METADATA as $preset_metadata ) { + $slugs = static::get_settings_slugs( $settings, $preset_metadata, $origins ); + foreach ( $preset_metadata['classes'] as $class => $property ) { + foreach ( $slugs as $slug ) { + $css_var = static::replace_slug_in_string( $preset_metadata['css_vars'], $slug ); + $class_name = static::replace_slug_in_string( $class, $slug ); + $stylesheet .= static::to_ruleset( + static::append_to_selector( $selector, $class_name ), + array( + array( + 'name' => $property, + 'value' => 'var(' . $css_var . ') !important', + ), + ) + ); + } + } + } + + return $stylesheet; + } + + /** + * Function that scopes a selector with another one. This works a bit like + * SCSS nesting except the `&` operator isn't supported. + * + * + * $scope = '.a, .b .c'; + * $selector = '> .x, .y'; + * $merged = scope_selector( $scope, $selector ); + * // $merged is '.a > .x, .a .y, .b .c > .x, .b .c .y' + * + * + * @since 5.9.0 + * + * @param string $scope Selector to scope to. + * @param string $selector Original selector. + * @return string Scoped selector. + */ + protected static function scope_selector( $scope, $selector ) { + $scopes = explode( ',', $scope ); + $selectors = explode( ',', $selector ); + + $selectors_scoped = array(); + foreach ( $scopes as $outer ) { + foreach ( $selectors as $inner ) { + $outer = trim( $outer ); + $inner = trim( $inner ); + if ( ! empty( $outer ) && ! empty( $inner ) ) { + $selectors_scoped[] = $outer . ' ' . $inner; + } elseif ( empty( $outer ) ) { + $selectors_scoped[] = $inner; + } elseif ( empty( $inner ) ) { + $selectors_scoped[] = $outer; + } + } + } + + $result = implode( ', ', $selectors_scoped ); + return $result; + } + + /** + * Gets preset values keyed by slugs based on settings and metadata. + * + * + * $settings = array( + * 'typography' => array( + * 'fontFamilies' => array( + * array( + * 'slug' => 'sansSerif', + * 'fontFamily' => '"Helvetica Neue", sans-serif', + * ), + * array( + * 'slug' => 'serif', + * 'colors' => 'Georgia, serif', + * ) + * ), + * ), + * ); + * $meta = array( + * 'path' => array( 'typography', 'fontFamilies' ), + * 'value_key' => 'fontFamily', + * ); + * $values_by_slug = get_settings_values_by_slug(); + * // $values_by_slug === array( + * // 'sans-serif' => '"Helvetica Neue", sans-serif', + * // 'serif' => 'Georgia, serif', + * // ); + * + * + * @since 5.9.0 + * + * @param array $settings Settings to process. + * @param array $preset_metadata One of the PRESETS_METADATA values. + * @param array $origins List of origins to process. + * @return array Array of presets where each key is a slug and each value is the preset value. + */ + protected static function get_settings_values_by_slug( $settings, $preset_metadata, $origins ) { + $preset_per_origin = _wp_array_get( $settings, $preset_metadata['path'], array() ); + + $result = array(); + foreach ( $origins as $origin ) { + if ( ! isset( $preset_per_origin[ $origin ] ) ) { + continue; + } + foreach ( $preset_per_origin[ $origin ] as $preset ) { + $slug = _wp_to_kebab_case( $preset['slug'] ); + + $value = ''; + if ( isset( $preset_metadata['value_key'], $preset[ $preset_metadata['value_key'] ] ) ) { + $value_key = $preset_metadata['value_key']; + $value = $preset[ $value_key ]; + } elseif ( + isset( $preset_metadata['value_func'] ) && + is_callable( $preset_metadata['value_func'] ) + ) { + $value_func = $preset_metadata['value_func']; + $value = call_user_func( $value_func, $preset ); + } else { + // If we don't have a value, then don't add it to the result. + continue; + } + + $result[ $slug ] = $value; + } + } + return $result; + } + + /** + * Similar to get_settings_values_by_slug, but doesn't compute the value. + * + * @since 5.9.0 + * + * @param array $settings Settings to process. + * @param array $preset_metadata One of the PRESETS_METADATA values. + * @param array $origins List of origins to process. + * @return array Array of presets where the key and value are both the slug. + */ + protected static function get_settings_slugs( $settings, $preset_metadata, $origins = null ) { + if ( null === $origins ) { + $origins = static::VALID_ORIGINS; + } + + $preset_per_origin = _wp_array_get( $settings, $preset_metadata['path'], array() ); + + $result = array(); + foreach ( $origins as $origin ) { + if ( ! isset( $preset_per_origin[ $origin ] ) ) { + continue; + } + foreach ( $preset_per_origin[ $origin ] as $preset ) { + $slug = _wp_to_kebab_case( $preset['slug'] ); + + // Use the array as a set so we don't get duplicates. + $result[ $slug ] = $slug; + } + } + return $result; + } + + /** + * Transforms a slug into a CSS Custom Property. + * + * @since 5.9.0 + * + * @param string $input String to replace. + * @param string $slug The slug value to use to generate the custom property. + * @return string The CSS Custom Property. Something along the lines of `--wp--preset--color--black`. + */ + protected static function replace_slug_in_string( $input, $slug ) { + return strtr( $input, array( '$slug' => $slug ) ); + } + + /** + * Given the block settings, extracts the CSS Custom Properties + * for the presets and adds them to the $declarations array + * following the format: + * + * ```php + * array( + * 'name' => 'property_name', + * 'value' => 'property_value, + * ) + * ``` + * + * @since 5.8.0 + * @since 5.9.0 Added the `$origins` parameter. + * + * @param array $settings Settings to process. + * @param array $origins List of origins to process. + * @return array The modified $declarations. + */ + protected static function compute_preset_vars( $settings, $origins ) { + $declarations = array(); + foreach ( static::PRESETS_METADATA as $preset_metadata ) { + $values_by_slug = static::get_settings_values_by_slug( $settings, $preset_metadata, $origins ); + foreach ( $values_by_slug as $slug => $value ) { + $declarations[] = array( + 'name' => static::replace_slug_in_string( $preset_metadata['css_vars'], $slug ), + 'value' => $value, + ); + } + } + + return $declarations; + } + + /** + * Given an array of settings, extracts the CSS Custom Properties + * for the custom values and adds them to the $declarations + * array following the format: + * + * ```php + * array( + * 'name' => 'property_name', + * 'value' => 'property_value, + * ) + * ``` + * + * @since 5.8.0 + * + * @param array $settings Settings to process. + * @return array The modified $declarations. + */ + protected static function compute_theme_vars( $settings ) { + $declarations = array(); + $custom_values = _wp_array_get( $settings, array( 'custom' ), array() ); + $css_vars = static::flatten_tree( $custom_values ); + foreach ( $css_vars as $key => $value ) { + $declarations[] = array( + 'name' => '--wp--custom--' . $key, + 'value' => $value, + ); + } + + return $declarations; + } + + /** + * Given a tree, it creates a flattened one + * by merging the keys and binding the leaf values + * to the new keys. + * + * It also transforms camelCase names into kebab-case + * and substitutes '/' by '-'. + * + * This is thought to be useful to generate + * CSS Custom Properties from a tree, + * although there's nothing in the implementation + * of this function that requires that format. + * + * For example, assuming the given prefix is '--wp' + * and the token is '--', for this input tree: + * + * { + * 'some/property': 'value', + * 'nestedProperty': { + * 'sub-property': 'value' + * } + * } + * + * it'll return this output: + * + * { + * '--wp--some-property': 'value', + * '--wp--nested-property--sub-property': 'value' + * } + * + * @since 5.8.0 + * + * @param array $tree Input tree to process. + * @param string $prefix Optional. Prefix to prepend to each variable. Default empty string. + * @param string $token Optional. Token to use between levels. Default '--'. + * @return array The flattened tree. + */ + protected static function flatten_tree( $tree, $prefix = '', $token = '--' ) { + $result = array(); + foreach ( $tree as $property => $value ) { + $new_key = $prefix . str_replace( + '/', + '-', + strtolower( _wp_to_kebab_case( $property ) ) + ); + + if ( is_array( $value ) ) { + $new_prefix = $new_key . $token; + $flattened_subtree = static::flatten_tree( $value, $new_prefix, $token ); + foreach ( $flattened_subtree as $subtree_key => $subtree_value ) { + $result[ $subtree_key ] = $subtree_value; + } + } else { + $result[ $new_key ] = $value; + } + } + return $result; + } + + /** + * Given a styles array, it extracts the style properties + * and adds them to the $declarations array following the format: + * + * ```php + * array( + * 'name' => 'property_name', + * 'value' => 'property_value, + * ) + * ``` + * + * @since 5.8.0 + * @since 5.9.0 Added the `$settings` and `$properties` parameters. + * @since 6.1.0 Added `$theme_json`, `$selector`, and `$use_root_padding` parameters. + * + * @param array $styles Styles to process. + * @param array $settings Theme settings. + * @param array $properties Properties metadata. + * @param array $theme_json Theme JSON array. + * @param string $selector The style block selector. + * @param boolean $use_root_padding Whether to add custom properties at root level. + * @return array Returns the modified $declarations. + */ + protected static function compute_style_properties( $styles, $settings = array(), $properties = null, $theme_json = null, $selector = null, $use_root_padding = null ) { + if ( null === $properties ) { + $properties = static::PROPERTIES_METADATA; + } + + $declarations = array(); + if ( empty( $styles ) ) { + return $declarations; + } + + $root_variable_duplicates = array(); + + foreach ( $properties as $css_property => $value_path ) { + $value = static::get_property_value( $styles, $value_path, $theme_json ); + + if ( str_starts_with( $css_property, '--wp--style--root--' ) && ( static::ROOT_BLOCK_SELECTOR !== $selector || ! $use_root_padding ) ) { + continue; + } + // Root-level padding styles don't currently support strings with CSS shorthand values. + // This may change: https://github.com/WordPress/gutenberg/issues/40132. + if ( '--wp--style--root--padding' === $css_property && is_string( $value ) ) { + continue; + } + + if ( str_starts_with( $css_property, '--wp--style--root--' ) && $use_root_padding ) { + $root_variable_duplicates[] = substr( $css_property, strlen( '--wp--style--root--' ) ); + } + + // Look up protected properties, keyed by value path. + // Skip protected properties that are explicitly set to `null`. + if ( is_array( $value_path ) ) { + $path_string = implode( '.', $value_path ); + if ( + // TODO: Replace array_key_exists() with isset() check once WordPress drops + // support for PHP 5.6. See https://core.trac.wordpress.org/ticket/57067. + array_key_exists( $path_string, static::PROTECTED_PROPERTIES ) && + _wp_array_get( $settings, static::PROTECTED_PROPERTIES[ $path_string ], null ) === null + ) { + continue; + } + } + + // Skip if empty and not "0" or value represents array of longhand values. + $has_missing_value = empty( $value ) && ! is_numeric( $value ); + if ( $has_missing_value || is_array( $value ) ) { + continue; + } + + // Calculates fluid typography rules where available. + if ( 'font-size' === $css_property ) { + /* + * wp_get_typography_font_size_value() will check + * if fluid typography has been activated and also + * whether the incoming value can be converted to a fluid value. + * Values that already have a clamp() function will not pass the test, + * and therefore the original $value will be returned. + */ + $value = wp_get_typography_font_size_value( array( 'size' => $value ) ); + } + + $declarations[] = array( + 'name' => $css_property, + 'value' => $value, + ); + } + + // If a variable value is added to the root, the corresponding property should be removed. + foreach ( $root_variable_duplicates as $duplicate ) { + $discard = array_search( $duplicate, array_column( $declarations, 'name' ), true ); + if ( is_numeric( $discard ) ) { + array_splice( $declarations, $discard, 1 ); + } + } + + return $declarations; + } + + /** + * Returns the style property for the given path. + * + * It also converts CSS Custom Property stored as + * "var:preset|color|secondary" to the form + * "--wp--preset--color--secondary". + * + * It also converts references to a path to the value + * stored at that location, e.g. + * { "ref": "style.color.background" } => "#fff". + * + * @since 5.8.0 + * @since 5.9.0 Added support for values of array type, which are returned as is. + * @since 6.1.0 Added the `$theme_json` parameter. + * + * @param array $styles Styles subtree. + * @param array $path Which property to process. + * @param array $theme_json Theme JSON array. + * @return string|array Style property value. + */ + protected static function get_property_value( $styles, $path, $theme_json = null ) { + $value = _wp_array_get( $styles, $path, '' ); + + // Gutenberg didn't have this check. + if ( '' === $value || null === $value ) { + // No need to process the value further. + return ''; + } + + /* + * This converts references to a path to the value at that path + * where the values is an array with a "ref" key, pointing to a path. + * For example: { "ref": "style.color.background" } => "#fff". + */ + if ( is_array( $value ) && isset( $value['ref'] ) ) { + $value_path = explode( '.', $value['ref'] ); + $ref_value = _wp_array_get( $theme_json, $value_path ); + // Only use the ref value if we find anything. + if ( ! empty( $ref_value ) && is_string( $ref_value ) ) { + $value = $ref_value; + } + + if ( is_array( $ref_value ) && isset( $ref_value['ref'] ) ) { + $path_string = json_encode( $path ); + $ref_value_string = json_encode( $ref_value ); + _doing_it_wrong( + 'get_property_value', + sprintf( + /* translators: 1: theme.json, 2: Value name, 3: Value path, 4: Another value name. */ + __( 'Your %1$s file uses a dynamic value (%2$s) for the path at %3$s. However, the value at %3$s is also a dynamic value (pointing to %4$s) and pointing to another dynamic value is not supported. Please update %3$s to point directly to %4$s.', 'gutenberg' ), + 'theme.json', + $ref_value_string, + $path_string, + $ref_value['ref'] + ), + '6.1.0' + ); + } + } + + if ( is_array( $value ) ) { + return $value; + } + + // Convert custom CSS properties. + $prefix = 'var:'; + $prefix_len = strlen( $prefix ); + $token_in = '|'; + $token_out = '--'; + if ( 0 === strncmp( $value, $prefix, $prefix_len ) ) { + $unwrapped_name = str_replace( + $token_in, + $token_out, + substr( $value, $prefix_len ) + ); + $value = "var(--wp--$unwrapped_name)"; + } + + return $value; + } + + /** + * Builds metadata for the setting nodes, which returns in the form of: + * + * [ + * [ + * 'path' => ['path', 'to', 'some', 'node' ], + * 'selector' => 'CSS selector for some node' + * ], + * [ + * 'path' => [ 'path', 'to', 'other', 'node' ], + * 'selector' => 'CSS selector for other node' + * ], + * ] + * + * @since 5.8.0 + * + * @param array $theme_json The tree to extract setting nodes from. + * @param array $selectors List of selectors per block. + * @return array An array of setting nodes metadata. + */ + protected static function get_setting_nodes( $theme_json, $selectors = array() ) { + $nodes = array(); + if ( ! isset( $theme_json['settings'] ) ) { + return $nodes; + } + + // Top-level. + $nodes[] = array( + 'path' => array( 'settings' ), + 'selector' => static::ROOT_BLOCK_SELECTOR, + ); + + // Calculate paths for blocks. + if ( ! isset( $theme_json['settings']['blocks'] ) ) { + return $nodes; + } + + foreach ( $theme_json['settings']['blocks'] as $name => $node ) { + $selector = null; + if ( isset( $selectors[ $name ]['selector'] ) ) { + $selector = $selectors[ $name ]['selector']; + } + + $nodes[] = array( + 'path' => array( 'settings', 'blocks', $name ), + 'selector' => $selector, + ); + } + + return $nodes; + } + + /** + * Builds metadata for the style nodes, which returns in the form of: + * + * [ + * [ + * 'path' => [ 'path', 'to', 'some', 'node' ], + * 'selector' => 'CSS selector for some node', + * 'duotone' => 'CSS selector for duotone for some node' + * ], + * [ + * 'path' => ['path', 'to', 'other', 'node' ], + * 'selector' => 'CSS selector for other node', + * 'duotone' => null + * ], + * ] + * + * @since 5.8.0 + * + * @param array $theme_json The tree to extract style nodes from. + * @param array $selectors List of selectors per block. + * @return array An array of style nodes metadata. + */ + protected static function get_style_nodes( $theme_json, $selectors = array() ) { + $nodes = array(); + if ( ! isset( $theme_json['styles'] ) ) { + return $nodes; + } + + // Top-level. + $nodes[] = array( + 'path' => array( 'styles' ), + 'selector' => static::ROOT_BLOCK_SELECTOR, + ); + + if ( isset( $theme_json['styles']['elements'] ) ) { + foreach ( self::ELEMENTS as $element => $selector ) { + if ( ! isset( $theme_json['styles']['elements'][ $element ] ) || ! array_key_exists( $element, static::ELEMENTS ) ) { + continue; + } + + // Handle element defaults. + $nodes[] = array( + 'path' => array( 'styles', 'elements', $element ), + 'selector' => static::ELEMENTS[ $element ], + ); + + // Handle any pseudo selectors for the element. + // TODO: Replace array_key_exists() with isset() check once WordPress drops + // support for PHP 5.6. See https://core.trac.wordpress.org/ticket/57067. + if ( array_key_exists( $element, static::VALID_ELEMENT_PSEUDO_SELECTORS ) ) { + foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] as $pseudo_selector ) { + + if ( isset( $theme_json['styles']['elements'][ $element ][ $pseudo_selector ] ) ) { + $nodes[] = array( + 'path' => array( 'styles', 'elements', $element ), + 'selector' => static::append_to_selector( static::ELEMENTS[ $element ], $pseudo_selector ), + ); + } + } + } + } + } + + // Blocks. + if ( ! isset( $theme_json['styles']['blocks'] ) ) { + return $nodes; + } + + $block_nodes = static::get_block_nodes( $theme_json, $selectors ); + foreach ( $block_nodes as $block_node ) { + $nodes[] = $block_node; + } + + /** + * Filters the list of style nodes with metadata. + * + * This allows for things like loading block CSS independently. + * + * @since 6.1.0 + * + * @param array $nodes Style nodes with metadata. + */ + return apply_filters( 'wp_theme_json_get_style_nodes', $nodes ); + } + + /** + * A public helper to get the block nodes from a theme.json file. + * + * @since 6.1.0 + * + * @return array The block nodes in theme.json. + */ + public function get_styles_block_nodes() { + return static::get_block_nodes( $this->theme_json ); + } + + /** + * Returns a filtered declarations array if there is a separator block with only a background + * style defined in theme.json by adding a color attribute to reflect the changes in the front. + * + * @since 6.1.1 + * + * @param array $declarations List of declarations. + * @return array $declarations List of declarations filtered. + */ + private static function update_separator_declarations( $declarations ) { + // Gutenberg and core implementation differed. + // https://github.com/WordPress/gutenberg/pull/44943. + $background_color = ''; + $border_color_matches = false; + $text_color_matches = false; + + foreach ( $declarations as $declaration ) { + if ( 'background-color' === $declaration['name'] && ! $background_color && isset( $declaration['value'] ) ) { + $background_color = $declaration['value']; + } elseif ( 'border-color' === $declaration['name'] ) { + $border_color_matches = true; + } elseif ( 'color' === $declaration['name'] ) { + $text_color_matches = true; + } + + if ( $background_color && $border_color_matches && $text_color_matches ) { + break; + } + } + + if ( $background_color && ! $border_color_matches && ! $text_color_matches ) { + $declarations[] = array( + 'name' => 'color', + 'value' => $background_color, + ); + } + + return $declarations; + } + + /** + * An internal method to get the block nodes from a theme.json file. + * + * @since 6.1.0 + * + * @param array $theme_json The theme.json converted to an array. + * @param array $selectors Optional list of selectors per block. + * @return array The block nodes in theme.json. + */ + private static function get_block_nodes( $theme_json, $selectors = array() ) { + $selectors = empty( $selectors ) ? static::get_blocks_metadata() : $selectors; + $nodes = array(); + if ( ! isset( $theme_json['styles'] ) ) { + return $nodes; + } + + // Blocks. + if ( ! isset( $theme_json['styles']['blocks'] ) ) { + return $nodes; + } + + foreach ( $theme_json['styles']['blocks'] as $name => $node ) { + $selector = null; + if ( isset( $selectors[ $name ]['selector'] ) ) { + $selector = $selectors[ $name ]['selector']; + } + + $duotone_selector = null; + if ( isset( $selectors[ $name ]['duotone'] ) ) { + $duotone_selector = $selectors[ $name ]['duotone']; + } + + $feature_selectors = null; + if ( isset( $selectors[ $name ]['features'] ) ) { + $feature_selectors = $selectors[ $name ]['features']; + } + + $nodes[] = array( + 'name' => $name, + 'path' => array( 'styles', 'blocks', $name ), + 'selector' => $selector, + 'duotone' => $duotone_selector, + 'features' => $feature_selectors, + ); + + if ( isset( $theme_json['styles']['blocks'][ $name ]['elements'] ) ) { + foreach ( $theme_json['styles']['blocks'][ $name ]['elements'] as $element => $node ) { + $nodes[] = array( + 'path' => array( 'styles', 'blocks', $name, 'elements', $element ), + 'selector' => $selectors[ $name ]['elements'][ $element ], + ); + + // Handle any pseudo selectors for the element. + // TODO: Replace array_key_exists() with isset() check once WordPress drops + // support for PHP 5.6. See https://core.trac.wordpress.org/ticket/57067. + if ( array_key_exists( $element, static::VALID_ELEMENT_PSEUDO_SELECTORS ) ) { + foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] as $pseudo_selector ) { + if ( isset( $theme_json['styles']['blocks'][ $name ]['elements'][ $element ][ $pseudo_selector ] ) ) { + $nodes[] = array( + 'path' => array( 'styles', 'blocks', $name, 'elements', $element ), + 'selector' => static::append_to_selector( $selectors[ $name ]['elements'][ $element ], $pseudo_selector ), + ); + } + } + } + } + } + } + + return $nodes; + } + + /** + * Gets the CSS rules for a particular block from theme.json. + * + * @since 6.1.0 + * + * @param array $block_metadata Metadata about the block to get styles for. + * + * @return string Styles for the block. + */ + public function get_styles_for_block( $block_metadata ) { + $node = _wp_array_get( $this->theme_json, $block_metadata['path'], array() ); + $use_root_padding = isset( $this->theme_json['settings']['useRootPaddingAwareAlignments'] ) && true === $this->theme_json['settings']['useRootPaddingAwareAlignments']; + $selector = $block_metadata['selector']; + $settings = _wp_array_get( $this->theme_json, array( 'settings' ) ); + + /* + * Process style declarations for block support features the current + * block contains selectors for. Values for a feature with a custom + * selector are filtered from the theme.json node before it is + * processed as normal. + */ + $feature_declarations = array(); + + if ( ! empty( $block_metadata['features'] ) ) { + foreach ( $block_metadata['features'] as $feature_name => $feature_selector ) { + if ( ! empty( $node[ $feature_name ] ) ) { + // Create temporary node containing only the feature data + // to leverage existing `compute_style_properties` function. + $feature = array( $feature_name => $node[ $feature_name ] ); + // Generate the feature's declarations only. + $new_feature_declarations = static::compute_style_properties( $feature, $settings, null, $this->theme_json ); + + // Merge new declarations with any that already exist for + // the feature selector. This may occur when multiple block + // support features use the same custom selector. + if ( isset( $feature_declarations[ $feature_selector ] ) ) { + foreach ( $new_feature_declarations as $new_feature_declaration ) { + $feature_declarations[ $feature_selector ][] = $new_feature_declaration; + } + } else { + $feature_declarations[ $feature_selector ] = $new_feature_declarations; + } + + // Remove the feature from the block's node now the + // styles will be included under the feature level selector. + unset( $node[ $feature_name ] ); + } + } + } + + /* + * Get a reference to element name from path. + * $block_metadata['path'] = array( 'styles','elements','link' ); + * Make sure that $block_metadata['path'] describes an element node, like [ 'styles', 'element', 'link' ]. + * Skip non-element paths like just ['styles']. + */ + $is_processing_element = in_array( 'elements', $block_metadata['path'], true ); + + $current_element = $is_processing_element ? $block_metadata['path'][ count( $block_metadata['path'] ) - 1 ] : null; + + $element_pseudo_allowed = array(); + + // TODO: Replace array_key_exists() with isset() check once WordPress drops + // support for PHP 5.6. See https://core.trac.wordpress.org/ticket/57067. + if ( array_key_exists( $current_element, static::VALID_ELEMENT_PSEUDO_SELECTORS ) ) { + $element_pseudo_allowed = static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ]; + } + + /* + * Check for allowed pseudo classes (e.g. ":hover") from the $selector ("a:hover"). + * This also resets the array keys. + */ + $pseudo_matches = array_values( + array_filter( + $element_pseudo_allowed, + function( $pseudo_selector ) use ( $selector ) { + return str_contains( $selector, $pseudo_selector ); + } + ) + ); + + $pseudo_selector = isset( $pseudo_matches[0] ) ? $pseudo_matches[0] : null; + + /* + * If the current selector is a pseudo selector that's defined in the allow list for the current + * element then compute the style properties for it. + * Otherwise just compute the styles for the default selector as normal. + */ + if ( $pseudo_selector && isset( $node[ $pseudo_selector ] ) && + // TODO: Replace array_key_exists() with isset() check once WordPress drops + // support for PHP 5.6. See https://core.trac.wordpress.org/ticket/57067. + array_key_exists( $current_element, static::VALID_ELEMENT_PSEUDO_SELECTORS ) + && in_array( $pseudo_selector, static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ], true ) + ) { + $declarations = static::compute_style_properties( $node[ $pseudo_selector ], $settings, null, $this->theme_json, $selector, $use_root_padding ); + } else { + $declarations = static::compute_style_properties( $node, $settings, null, $this->theme_json, $selector, $use_root_padding ); + } + + $block_rules = ''; + + /* + * 1. Separate the declarations that use the general selector + * from the ones using the duotone selector. + */ + $declarations_duotone = array(); + foreach ( $declarations as $index => $declaration ) { + if ( 'filter' === $declaration['name'] ) { + unset( $declarations[ $index ] ); + $declarations_duotone[] = $declaration; + } + } + + // Update declarations if there are separators with only background color defined. + if ( '.wp-block-separator' === $selector ) { + $declarations = static::update_separator_declarations( $declarations ); + } + + // 2. Generate and append the rules that use the general selector. + $block_rules .= static::to_ruleset( $selector, $declarations ); + + // 3. Generate and append the rules that use the duotone selector. + if ( isset( $block_metadata['duotone'] ) && ! empty( $declarations_duotone ) ) { + $selector_duotone = static::scope_selector( $block_metadata['selector'], $block_metadata['duotone'] ); + $block_rules .= static::to_ruleset( $selector_duotone, $declarations_duotone ); + } + + // 4. Generate Layout block gap styles. + if ( + static::ROOT_BLOCK_SELECTOR !== $selector && + ! empty( $block_metadata['name'] ) + ) { + $block_rules .= $this->get_layout_styles( $block_metadata ); + } + + // 5. Generate and append the feature level rulesets. + foreach ( $feature_declarations as $feature_selector => $individual_feature_declarations ) { + $block_rules .= static::to_ruleset( $feature_selector, $individual_feature_declarations ); + } + + return $block_rules; + } + + /** + * Outputs the CSS for layout rules on the root. + * + * @since 6.1.0 + * + * @param string $selector The root node selector. + * @param array $block_metadata The metadata for the root block. + * @return string The additional root rules CSS. + */ + public function get_root_layout_rules( $selector, $block_metadata ) { + $css = ''; + $settings = _wp_array_get( $this->theme_json, array( 'settings' ) ); $use_root_padding = isset( $this->theme_json['settings']['useRootPaddingAwareAlignments'] ) && true === $this->theme_json['settings']['useRootPaddingAwareAlignments']; /* @@ -960,413 +2324,836 @@ public function get_root_layout_rules( $selector, $block_metadata ) { */ $css .= 'body { margin: 0;'; - /* - * If there are content and wide widths in theme.json, output them - * as custom properties on the body element so all blocks can use them. - */ - if ( isset( $settings['layout']['contentSize'] ) || isset( $settings['layout']['wideSize'] ) ) { - $content_size = isset( $settings['layout']['contentSize'] ) ? $settings['layout']['contentSize'] : $settings['layout']['wideSize']; - $content_size = static::is_safe_css_declaration( 'max-width', $content_size ) ? $content_size : 'initial'; - $wide_size = isset( $settings['layout']['wideSize'] ) ? $settings['layout']['wideSize'] : $settings['layout']['contentSize']; - $wide_size = static::is_safe_css_declaration( 'max-width', $wide_size ) ? $wide_size : 'initial'; - $css .= '--wp--style--global--content-size: ' . $content_size . ';'; - $css .= '--wp--style--global--wide-size: ' . $wide_size . ';'; + /* + * If there are content and wide widths in theme.json, output them + * as custom properties on the body element so all blocks can use them. + */ + if ( isset( $settings['layout']['contentSize'] ) || isset( $settings['layout']['wideSize'] ) ) { + $content_size = isset( $settings['layout']['contentSize'] ) ? $settings['layout']['contentSize'] : $settings['layout']['wideSize']; + $content_size = static::is_safe_css_declaration( 'max-width', $content_size ) ? $content_size : 'initial'; + $wide_size = isset( $settings['layout']['wideSize'] ) ? $settings['layout']['wideSize'] : $settings['layout']['contentSize']; + $wide_size = static::is_safe_css_declaration( 'max-width', $wide_size ) ? $wide_size : 'initial'; + $css .= '--wp--style--global--content-size: ' . $content_size . ';'; + $css .= '--wp--style--global--wide-size: ' . $wide_size . ';'; + } + + $css .= '}'; + + if ( $use_root_padding ) { + // Top and bottom padding are applied to the outer block container. + $css .= '.wp-site-blocks { padding-top: var(--wp--style--root--padding-top); padding-bottom: var(--wp--style--root--padding-bottom); }'; + // Right and left padding are applied to the first container with `.has-global-padding` class. + $css .= '.has-global-padding { padding-right: var(--wp--style--root--padding-right); padding-left: var(--wp--style--root--padding-left); }'; + // Nested containers with `.has-global-padding` class do not get padding. + $css .= '.has-global-padding :where(.has-global-padding) { padding-right: 0; padding-left: 0; }'; + // Alignfull children of the container with left and right padding have negative margins so they can still be full width. + $css .= '.has-global-padding > .alignfull { margin-right: calc(var(--wp--style--root--padding-right) * -1); margin-left: calc(var(--wp--style--root--padding-left) * -1); }'; + // The above rule is negated for alignfull children of nested containers. + $css .= '.has-global-padding :where(.has-global-padding) > .alignfull { margin-right: 0; margin-left: 0; }'; + // Some of the children of alignfull blocks without content width should also get padding: text blocks and non-alignfull container blocks. + $css .= '.has-global-padding > .alignfull:where(:not(.has-global-padding)) > :where([class*="wp-block-"]:not(.alignfull):not([class*="__"]),p,h1,h2,h3,h4,h5,h6,ul,ol) { padding-right: var(--wp--style--root--padding-right); padding-left: var(--wp--style--root--padding-left); }'; + // The above rule also has to be negated for blocks inside nested `.has-global-padding` blocks. + $css .= '.has-global-padding :where(.has-global-padding) > .alignfull:where(:not(.has-global-padding)) > :where([class*="wp-block-"]:not(.alignfull):not([class*="__"]),p,h1,h2,h3,h4,h5,h6,ul,ol) { padding-right: 0; padding-left: 0; }'; + } + + $css .= '.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }'; + $css .= '.wp-site-blocks > .alignright { float: right; margin-left: 2em; }'; + $css .= '.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }'; + + $block_gap_value = _wp_array_get( $this->theme_json, array( 'styles', 'spacing', 'blockGap' ), '0.5em' ); + $has_block_gap_support = _wp_array_get( $this->theme_json, array( 'settings', 'spacing', 'blockGap' ) ) !== null; + if ( $has_block_gap_support ) { + $block_gap_value = static::get_property_value( $this->theme_json, array( 'styles', 'spacing', 'blockGap' ) ); + $css .= '.wp-site-blocks > * { margin-block-start: 0; margin-block-end: 0; }'; + $css .= ".wp-site-blocks > * + * { margin-block-start: $block_gap_value; }"; + + // For backwards compatibility, ensure the legacy block gap CSS variable is still available. + $css .= "$selector { --wp--style--block-gap: $block_gap_value; }"; + } + $css .= $this->get_layout_styles( $block_metadata ); + + return $css; + } + + /** + * For metadata values that can either be booleans or paths to booleans, gets the value. + * + * ```php + * $data = array( + * 'color' => array( + * 'defaultPalette' => true + * ) + * ); + * + * static::get_metadata_boolean( $data, false ); + * // => false + * + * static::get_metadata_boolean( $data, array( 'color', 'defaultPalette' ) ); + * // => true + * ``` + * + * @since 6.0.0 + * + * @param array $data The data to inspect. + * @param bool|array $path Boolean or path to a boolean. + * @param bool $default Default value if the referenced path is missing. + * Default false. + * @return bool Value of boolean metadata. + */ + protected static function get_metadata_boolean( $data, $path, $default = false ) { + if ( is_bool( $path ) ) { + return $path; + } + + if ( is_array( $path ) ) { + $value = _wp_array_get( $data, $path ); + if ( null !== $value ) { + return $value; + } + } + + return $default; + } + + /** + * Merges new incoming data. + * + * @since 5.8.0 + * @since 5.9.0 Duotone preset also has origins. + * + * @param WP_Theme_JSON $incoming Data to merge. + */ + public function merge( $incoming ) { + $incoming_data = $incoming->get_raw_data(); + $this->theme_json = array_replace_recursive( $this->theme_json, $incoming_data ); + + /* + * The array_replace_recursive algorithm merges at the leaf level, + * but we don't want leaf arrays to be merged, so we overwrite it. + * + * For leaf values that are sequential arrays it will use the numeric indexes for replacement. + * We rather replace the existing with the incoming value, if it exists. + * This is the case of spacing.units. + * + * For leaf values that are associative arrays it will merge them as expected. + * This is also not the behavior we want for the current associative arrays (presets). + * We rather replace the existing with the incoming value, if it exists. + * This happens, for example, when we merge data from theme.json upon existing + * theme supports or when we merge anything coming from the same source twice. + * This is the case of color.palette, color.gradients, color.duotone, + * typography.fontSizes, or typography.fontFamilies. + * + * Additionally, for some preset types, we also want to make sure the + * values they introduce don't conflict with default values. We do so + * by checking the incoming slugs for theme presets and compare them + * with the equivalent default presets: if a slug is present as a default + * we remove it from the theme presets. + */ + $nodes = static::get_setting_nodes( $incoming_data ); + $slugs_global = static::get_default_slugs( $this->theme_json, array( 'settings' ) ); + foreach ( $nodes as $node ) { + // Replace the spacing.units. + $path = $node['path']; + $path[] = 'spacing'; + $path[] = 'units'; + + $content = _wp_array_get( $incoming_data, $path, null ); + if ( isset( $content ) ) { + _wp_array_set( $this->theme_json, $path, $content ); + } + + // Replace the presets. + foreach ( static::PRESETS_METADATA as $preset ) { + $override_preset = ! static::get_metadata_boolean( $this->theme_json['settings'], $preset['prevent_override'], true ); + + foreach ( static::VALID_ORIGINS as $origin ) { + $base_path = $node['path']; + foreach ( $preset['path'] as $leaf ) { + $base_path[] = $leaf; + } + + $path = $base_path; + $path[] = $origin; + + $content = _wp_array_get( $incoming_data, $path, null ); + if ( ! isset( $content ) ) { + continue; + } + + if ( 'theme' === $origin && $preset['use_default_names'] ) { + foreach ( $content as $key => $item ) { + if ( ! isset( $item['name'] ) ) { + $name = static::get_name_from_defaults( $item['slug'], $base_path ); + if ( null !== $name ) { + $content[ $key ]['name'] = $name; + } + } + } + } + + if ( + ( 'theme' !== $origin ) || + ( 'theme' === $origin && $override_preset ) + ) { + _wp_array_set( $this->theme_json, $path, $content ); + } else { + $slugs_node = static::get_default_slugs( $this->theme_json, $node['path'] ); + $slugs = array_merge_recursive( $slugs_global, $slugs_node ); + + $slugs_for_preset = _wp_array_get( $slugs, $preset['path'], array() ); + $content = static::filter_slugs( $content, $slugs_for_preset ); + _wp_array_set( $this->theme_json, $path, $content ); + } + } + } } + } - $css .= '}'; + /** + * Converts all filter (duotone) presets into SVGs. + * + * @since 5.9.1 + * + * @param array $origins List of origins to process. + * @return string SVG filters. + */ + public function get_svg_filters( $origins ) { + $blocks_metadata = static::get_blocks_metadata(); + $setting_nodes = static::get_setting_nodes( $this->theme_json, $blocks_metadata ); - if ( $use_root_padding ) { - // Top and bottom padding are applied to the outer block container. - $css .= '.wp-site-blocks { padding-top: var(--wp--style--root--padding-top); padding-bottom: var(--wp--style--root--padding-bottom); }'; - // Right and left padding are applied to the first container with `.has-global-padding` class. - $css .= '.has-global-padding { padding-right: var(--wp--style--root--padding-right); padding-left: var(--wp--style--root--padding-left); }'; - // Nested containers with `.has-global-padding` class do not get padding. - $css .= '.has-global-padding :where(.has-global-padding) { padding-right: 0; padding-left: 0; }'; - // Alignfull children of the container with left and right padding have negative margins so they can still be full width. - $css .= '.has-global-padding > .alignfull { margin-right: calc(var(--wp--style--root--padding-right) * -1); margin-left: calc(var(--wp--style--root--padding-left) * -1); }'; - // The above rule is negated for alignfull children of nested containers. - $css .= '.has-global-padding :where(.has-global-padding) > .alignfull { margin-right: 0; margin-left: 0; }'; - // Some of the children of alignfull blocks without content width should also get padding: text blocks and non-alignfull container blocks. - $css .= '.has-global-padding > .alignfull:where(:not(.has-global-padding)) > :where([class*="wp-block-"]:not(.alignfull):not([class*="__"]),p,h1,h2,h3,h4,h5,h6,ul,ol) { padding-right: var(--wp--style--root--padding-right); padding-left: var(--wp--style--root--padding-left); }'; - // The above rule also has to be negated for blocks inside nested `.has-global-padding` blocks. - $css .= '.has-global-padding :where(.has-global-padding) > .alignfull:where(:not(.has-global-padding)) > :where([class*="wp-block-"]:not(.alignfull):not([class*="__"]),p,h1,h2,h3,h4,h5,h6,ul,ol) { padding-right: 0; padding-left: 0; }'; + $filters = ''; + foreach ( $setting_nodes as $metadata ) { + $node = _wp_array_get( $this->theme_json, $metadata['path'], array() ); + if ( empty( $node['color']['duotone'] ) ) { + continue; + } + + $duotone_presets = $node['color']['duotone']; + + foreach ( $origins as $origin ) { + if ( ! isset( $duotone_presets[ $origin ] ) ) { + continue; + } + foreach ( $duotone_presets[ $origin ] as $duotone_preset ) { + $filters .= wp_get_duotone_filter_svg( $duotone_preset ); + } + } } - $css .= '.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }'; - $css .= '.wp-site-blocks > .alignright { float: right; margin-left: 2em; }'; - $css .= '.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }'; + return $filters; + } - $block_gap_value = _wp_array_get( $this->theme_json, array( 'styles', 'spacing', 'blockGap' ), '0.5em' ); - $has_block_gap_support = _wp_array_get( $this->theme_json, array( 'settings', 'spacing', 'blockGap' ) ) !== null; - if ( $has_block_gap_support ) { - $block_gap_value = static::get_property_value( $this->theme_json, array( 'styles', 'spacing', 'blockGap' ) ); - $css .= '.wp-site-blocks > * { margin-block-start: 0; margin-block-end: 0; }'; - $css .= ".wp-site-blocks > * + * { margin-block-start: $block_gap_value; }"; + /** + * Determines whether a presets should be overridden or not. + * + * @since 5.9.0 + * @deprecated 6.0.0 Use {@see 'get_metadata_boolean'} instead. + * + * @param array $theme_json The theme.json like structure to inspect. + * @param array $path Path to inspect. + * @param bool|array $override Data to compute whether to override the preset. + * @return boolean + */ + protected static function should_override_preset( $theme_json, $path, $override ) { + _deprecated_function( __METHOD__, '6.0.0', 'get_metadata_boolean' ); - // For backwards compatibility, ensure the legacy block gap CSS variable is still available. - $css .= "$selector { --wp--style--block-gap: $block_gap_value; }"; + if ( is_bool( $override ) ) { + return $override; } - $css .= $this->get_layout_styles( $block_metadata ); - return $css; + /* + * The relationship between whether to override the defaults + * and whether the defaults are enabled is inverse: + * + * - If defaults are enabled => theme presets should not be overridden + * - If defaults are disabled => theme presets should be overridden + * + * For example, a theme sets defaultPalette to false, + * making the default palette hidden from the user. + * In that case, we want all the theme presets to be present, + * so they should override the defaults. + */ + if ( is_array( $override ) ) { + $value = _wp_array_get( $theme_json, array_merge( $path, $override ) ); + if ( isset( $value ) ) { + return ! $value; + } + + // Search the top-level key if none was found for this node. + $value = _wp_array_get( $theme_json, array_merge( array( 'settings' ), $override ) ); + if ( isset( $value ) ) { + return ! $value; + } + + return true; + } } /** - * Converts each style section into a list of rulesets - * containing the block styles to be appended to the stylesheet. + * Returns the default slugs for all the presets in an associative array + * whose keys are the preset paths and the leafs is the list of slugs. * - * See glossary at https://developer.mozilla.org/en-US/docs/Web/CSS/Syntax + * For example: * - * For each section this creates a new ruleset such as: + * array( + * 'color' => array( + * 'palette' => array( 'slug-1', 'slug-2' ), + * 'gradients' => array( 'slug-3', 'slug-4' ), + * ), + * ) * - * block-selector { - * style-property-one: value; - * } + * @since 5.9.0 * - * @param array $style_nodes Nodes with styles. - * @return string The new stylesheet. + * @param array $data A theme.json like structure. + * @param array $node_path The path to inspect. It's 'settings' by default. + * @return array */ - protected function get_block_classes( $style_nodes ) { - $block_rules = ''; + protected static function get_default_slugs( $data, $node_path ) { + $slugs = array(); - foreach ( $style_nodes as $metadata ) { - if ( null === $metadata['selector'] ) { + foreach ( static::PRESETS_METADATA as $metadata ) { + $path = $node_path; + foreach ( $metadata['path'] as $leaf ) { + $path[] = $leaf; + } + $path[] = 'default'; + + $preset = _wp_array_get( $data, $path, null ); + if ( ! isset( $preset ) ) { continue; } - $block_rules .= static::get_styles_for_block( $metadata ); + + $slugs_for_preset = array(); + foreach ( $preset as $item ) { + if ( isset( $item['slug'] ) ) { + $slugs_for_preset[] = $item['slug']; + } + } + + _wp_array_set( $slugs, $metadata['path'], $slugs_for_preset ); } - return $block_rules; + return $slugs; } /** - * Given a styles array, it extracts the style properties - * and adds them to the $declarations array following the format: + * Gets a `default`'s preset name by a provided slug. * - * ```php - * array( - * 'name' => 'property_name', - * 'value' => 'property_value, - * ) - * ``` + * @since 5.9.0 * - * @param array $styles Styles to process. - * @param array $settings Theme settings. - * @param array $properties Properties metadata. - * @param array $theme_json Theme JSON array. - * @param string $selector The style block selector. - * @param boolean $use_root_padding Whether to add custom properties at root level. - * @return array Returns the modified $declarations. + * @param string $slug The slug we want to find a match from default presets. + * @param array $base_path The path to inspect. It's 'settings' by default. + * @return string|null */ - protected static function compute_style_properties( $styles, $settings = array(), $properties = null, $theme_json = null, $selector = null, $use_root_padding = null ) { - if ( null === $properties ) { - $properties = static::PROPERTIES_METADATA; + protected function get_name_from_defaults( $slug, $base_path ) { + $path = $base_path; + $path[] = 'default'; + $default_content = _wp_array_get( $this->theme_json, $path, null ); + if ( ! $default_content ) { + return null; + } + foreach ( $default_content as $item ) { + if ( $slug === $item['slug'] ) { + return $item['name']; + } + } + return null; + } + + /** + * Removes the preset values whose slug is equal to any of given slugs. + * + * @since 5.9.0 + * + * @param array $node The node with the presets to validate. + * @param array $slugs The slugs that should not be overridden. + * @return array The new node. + */ + protected static function filter_slugs( $node, $slugs ) { + if ( empty( $slugs ) ) { + return $node; } - $declarations = array(); - if ( empty( $styles ) ) { - return $declarations; + $new_node = array(); + foreach ( $node as $value ) { + if ( isset( $value['slug'] ) && ! in_array( $value['slug'], $slugs, true ) ) { + $new_node[] = $value; + } } - $root_variable_duplicates = array(); + return $new_node; + } - foreach ( $properties as $css_property => $value_path ) { - $value = static::get_property_value( $styles, $value_path, $theme_json ); + /** + * Removes insecure data from theme.json. + * + * @since 5.9.0 + * + * @param array $theme_json Structure to sanitize. + * @return array Sanitized structure. + */ + public static function remove_insecure_properties( $theme_json ) { + $sanitized = array(); - if ( str_starts_with( $css_property, '--wp--style--root--' ) && ( static::ROOT_BLOCK_SELECTOR !== $selector || ! $use_root_padding ) ) { + $theme_json = WP_Theme_JSON_Schema::migrate( $theme_json ); + + $valid_block_names = array_keys( static::get_blocks_metadata() ); + $valid_element_names = array_keys( static::ELEMENTS ); + + $theme_json = static::sanitize( $theme_json, $valid_block_names, $valid_element_names ); + + $blocks_metadata = static::get_blocks_metadata(); + $style_nodes = static::get_style_nodes( $theme_json, $blocks_metadata ); + + foreach ( $style_nodes as $metadata ) { + $input = _wp_array_get( $theme_json, $metadata['path'], array() ); + if ( empty( $input ) ) { continue; } - // Root-level padding styles don't currently support strings with CSS shorthand values. - // This may change: https://github.com/WordPress/gutenberg/issues/40132. - if ( '--wp--style--root--padding' === $css_property && is_string( $value ) ) { + + $output = static::remove_insecure_styles( $input ); + + /* + * Get a reference to element name from path. + * $metadata['path'] = array( 'styles', 'elements', 'link' ); + */ + $current_element = $metadata['path'][ count( $metadata['path'] ) - 1 ]; + + /* + * $output is stripped of pseudo selectors. Re-add and process them + * or insecure styles here. + */ + // TODO: Replace array_key_exists() with isset() check once WordPress drops + // support for PHP 5.6. See https://core.trac.wordpress.org/ticket/57067. + if ( array_key_exists( $current_element, static::VALID_ELEMENT_PSEUDO_SELECTORS ) ) { + foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ] as $pseudo_selector ) { + if ( isset( $input[ $pseudo_selector ] ) ) { + $output[ $pseudo_selector ] = static::remove_insecure_styles( $input[ $pseudo_selector ] ); + } + } + } + + if ( ! empty( $output ) ) { + _wp_array_set( $sanitized, $metadata['path'], $output ); + } + } + + $setting_nodes = static::get_setting_nodes( $theme_json ); + foreach ( $setting_nodes as $metadata ) { + $input = _wp_array_get( $theme_json, $metadata['path'], array() ); + if ( empty( $input ) ) { continue; } - if ( str_starts_with( $css_property, '--wp--style--root--' ) && $use_root_padding ) { - $root_variable_duplicates[] = substr( $css_property, strlen( '--wp--style--root--' ) ); + $output = static::remove_insecure_settings( $input ); + if ( ! empty( $output ) ) { + _wp_array_set( $sanitized, $metadata['path'], $output ); } + } - // Look up protected properties, keyed by value path. - // Skip protected properties that are explicitly set to `null`. - if ( is_array( $value_path ) ) { - $path_string = implode( '.', $value_path ); - if ( - array_key_exists( $path_string, static::PROTECTED_PROPERTIES ) && - _wp_array_get( $settings, static::PROTECTED_PROPERTIES[ $path_string ], null ) === null - ) { + if ( empty( $sanitized['styles'] ) ) { + unset( $theme_json['styles'] ); + } else { + $theme_json['styles'] = $sanitized['styles']; + } + + if ( empty( $sanitized['settings'] ) ) { + unset( $theme_json['settings'] ); + } else { + $theme_json['settings'] = $sanitized['settings']; + } + + return $theme_json; + } + + /** + * Processes a setting node and returns the same node + * without the insecure settings. + * + * @since 5.9.0 + * + * @param array $input Node to process. + * @return array + */ + protected static function remove_insecure_settings( $input ) { + $output = array(); + foreach ( static::PRESETS_METADATA as $preset_metadata ) { + foreach ( static::VALID_ORIGINS as $origin ) { + $path_with_origin = $preset_metadata['path']; + $path_with_origin[] = $origin; + $presets = _wp_array_get( $input, $path_with_origin, null ); + if ( null === $presets ) { continue; } - } - // Skip if empty and not "0" or value represents array of longhand values. - $has_missing_value = empty( $value ) && ! is_numeric( $value ); - if ( $has_missing_value || is_array( $value ) ) { - continue; - } + $escaped_preset = array(); + foreach ( $presets as $preset ) { + if ( + esc_attr( esc_html( $preset['name'] ) ) === $preset['name'] && + sanitize_html_class( $preset['slug'] ) === $preset['slug'] + ) { + $value = null; + if ( isset( $preset_metadata['value_key'], $preset[ $preset_metadata['value_key'] ] ) ) { + $value = $preset[ $preset_metadata['value_key'] ]; + } elseif ( + isset( $preset_metadata['value_func'] ) && + is_callable( $preset_metadata['value_func'] ) + ) { + $value = call_user_func( $preset_metadata['value_func'], $preset ); + } - // Calculates fluid typography rules where available. - if ( 'font-size' === $css_property ) { - /* - * gutenberg_get_typography_font_size_value() will check - * if fluid typography has been activated and also - * whether the incoming value can be converted to a fluid value. - * Values that already have a "clamp()" function will not pass the test, - * and therefore the original $value will be returned. - */ - $value = gutenberg_get_typography_font_size_value( array( 'size' => $value ) ); + $preset_is_valid = true; + foreach ( $preset_metadata['properties'] as $property ) { + if ( ! static::is_safe_css_declaration( $property, $value ) ) { + $preset_is_valid = false; + break; + } + } + + if ( $preset_is_valid ) { + $escaped_preset[] = $preset; + } + } + } + + if ( ! empty( $escaped_preset ) ) { + _wp_array_set( $output, $path_with_origin, $escaped_preset ); + } } + } + return $output; + } - $declarations[] = array( - 'name' => $css_property, - 'value' => $value, - ); + /** + * 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 ); + } + } } - // If a variable value is added to the root, the corresponding property should be removed. - foreach ( $root_variable_duplicates as $duplicate ) { - $discard = array_search( $duplicate, array_column( $declarations, 'name' ), true ); - if ( is_numeric( $discard ) ) { - array_splice( $declarations, $discard, 1 ); + // 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 $declarations; + return $output; } /** - * Returns the style property for the given path. + * Checks that a declaration provided by the user is safe. * - * It also converts CSS Custom Property stored as - * "var:preset|color|secondary" to the form - * "--wp--preset--color--secondary". + * @since 5.9.0 * - * It also converts references to a path to the value - * stored at that location, e.g. - * { "ref": "style.color.background" } => "#fff". + * @param string $property_name Property name in a CSS declaration, i.e. the `color` in `color: red`. + * @param string $property_value Value in a CSS declaration, i.e. the `red` in `color: red`. + * @return bool + */ + protected static function is_safe_css_declaration( $property_name, $property_value ) { + $style_to_validate = $property_name . ': ' . $property_value; + $filtered = esc_html( safecss_filter_attr( $style_to_validate ) ); + return ! empty( trim( $filtered ) ); + } + + /** + * Returns the raw data. * - * @param array $styles Styles subtree. - * @param array $path Which property to process. - * @param array $theme_json Theme JSON array. - * @return string|array|null Style property value. + * @since 5.8.0 + * + * @return array Raw data. */ - protected static function get_property_value( $styles, $path, $theme_json = null ) { - $value = _wp_array_get( $styles, $path ); + public function get_raw_data() { + return $this->theme_json; + } - // This converts references to a path to the value at that path - // where the values is an array with a "ref" key, pointing to a path. - // For example: { "ref": "style.color.background" } => "#fff". - if ( is_array( $value ) && array_key_exists( 'ref', $value ) ) { - $value_path = explode( '.', $value['ref'] ); - $ref_value = _wp_array_get( $theme_json, $value_path ); - // Only use the ref value if we find anything. - if ( ! empty( $ref_value ) && is_string( $ref_value ) ) { - $value = $ref_value; + /** + * Transforms the given editor settings according the + * add_theme_support format to the theme.json format. + * + * @since 5.8.0 + * + * @param array $settings Existing editor settings. + * @return array Config that adheres to the theme.json schema. + */ + public static function get_from_editor_settings( $settings ) { + $theme_settings = array( + 'version' => static::LATEST_SCHEMA, + 'settings' => array(), + ); + + // Deprecated theme supports. + if ( isset( $settings['disableCustomColors'] ) ) { + if ( ! isset( $theme_settings['settings']['color'] ) ) { + $theme_settings['settings']['color'] = array(); } + $theme_settings['settings']['color']['custom'] = ! $settings['disableCustomColors']; + } - if ( is_array( $ref_value ) && array_key_exists( 'ref', $ref_value ) ) { - $path_string = json_encode( $path ); - $ref_value_string = json_encode( $ref_value ); - _doing_it_wrong( 'get_property_value', "Your theme.json file uses a dynamic value ({$ref_value_string}) for the path at {$path_string}. However, the value at {$path_string} is also a dynamic value (pointing to {$ref_value['ref']}) and pointing to another dynamic value is not supported. Please update {$path_string} to point directly to {$ref_value['ref']}.", '6.1.0' ); + if ( isset( $settings['disableCustomGradients'] ) ) { + if ( ! isset( $theme_settings['settings']['color'] ) ) { + $theme_settings['settings']['color'] = array(); } + $theme_settings['settings']['color']['customGradient'] = ! $settings['disableCustomGradients']; } - if ( ! $value || is_array( $value ) ) { - return $value; + if ( isset( $settings['disableCustomFontSizes'] ) ) { + if ( ! isset( $theme_settings['settings']['typography'] ) ) { + $theme_settings['settings']['typography'] = array(); + } + $theme_settings['settings']['typography']['customFontSize'] = ! $settings['disableCustomFontSizes']; } - // Convert custom CSS properties. - $prefix = 'var:'; - $prefix_len = strlen( $prefix ); - $token_in = '|'; - $token_out = '--'; - if ( 0 === strncmp( $value, $prefix, $prefix_len ) ) { - $unwrapped_name = str_replace( - $token_in, - $token_out, - substr( $value, $prefix_len ) - ); - $value = "var(--wp--$unwrapped_name)"; + if ( isset( $settings['enableCustomLineHeight'] ) ) { + if ( ! isset( $theme_settings['settings']['typography'] ) ) { + $theme_settings['settings']['typography'] = array(); + } + $theme_settings['settings']['typography']['lineHeight'] = $settings['enableCustomLineHeight']; } - return $value; + if ( isset( $settings['enableCustomUnits'] ) ) { + if ( ! isset( $theme_settings['settings']['spacing'] ) ) { + $theme_settings['settings']['spacing'] = array(); + } + $theme_settings['settings']['spacing']['units'] = ( true === $settings['enableCustomUnits'] ) ? + array( 'px', 'em', 'rem', 'vh', 'vw', '%' ) : + $settings['enableCustomUnits']; + } + + if ( isset( $settings['colors'] ) ) { + if ( ! isset( $theme_settings['settings']['color'] ) ) { + $theme_settings['settings']['color'] = array(); + } + $theme_settings['settings']['color']['palette'] = $settings['colors']; + } + + if ( isset( $settings['gradients'] ) ) { + if ( ! isset( $theme_settings['settings']['color'] ) ) { + $theme_settings['settings']['color'] = array(); + } + $theme_settings['settings']['color']['gradients'] = $settings['gradients']; + } + + if ( isset( $settings['fontSizes'] ) ) { + $font_sizes = $settings['fontSizes']; + // Back-compatibility for presets without units. + foreach ( $font_sizes as $key => $font_size ) { + if ( is_numeric( $font_size['size'] ) ) { + $font_sizes[ $key ]['size'] = $font_size['size'] . 'px'; + } + } + if ( ! isset( $theme_settings['settings']['typography'] ) ) { + $theme_settings['settings']['typography'] = array(); + } + $theme_settings['settings']['typography']['fontSizes'] = $font_sizes; + } + + if ( isset( $settings['enableCustomSpacing'] ) ) { + if ( ! isset( $theme_settings['settings']['spacing'] ) ) { + $theme_settings['settings']['spacing'] = array(); + } + $theme_settings['settings']['spacing']['padding'] = $settings['enableCustomSpacing']; + } + + return $theme_settings; } /** - * Presets are a set of values that serve - * to bootstrap some styles: colors, font sizes, etc. - * - * They are a unkeyed array of values such as: - * - * ```php - * array( - * array( - * 'slug' => 'unique-name-within-the-set', - * 'name' => 'Name for the UI', - * => 'value' - * ), - * ) - * ``` + * Returns the current theme's wanted patterns(slugs) to be + * registered from Pattern Directory. * - * This contains the necessary metadata to process them: + * @since 6.0.0 * - * - path => Where to find the preset within the settings section. - * - prevent_override => Disables override of default presets by theme presets. - * The relationship between whether to override the defaults - * and whether the defaults are enabled is inverse: - * - If defaults are enabled => theme presets should not be overridden - * - If defaults are disabled => theme presets should be overridden - * For example, a theme sets defaultPalette to false, - * making the default palette hidden from the user. - * In that case, we want all the theme presets to be present, - * so they should override the defaults by setting this false. - * - use_default_names => whether to use the default names - * - value_key => the key that represents the value - * - value_func => optionally, instead of value_key, a function to generate - * the value that takes a preset as an argument - * (either value_key or value_func should be present) - * - css_vars => template string to use in generating the CSS Custom Property. - * Example output: "--wp--preset--duotone--blue: " will generate as many CSS Custom Properties as presets defined - * substituting the $slug for the slug's value for each preset value. - * - classes => array containing a structure with the classes to - * generate for the presets, where for each array item - * the key is the class name and the value the property name. - * The "$slug" substring will be replaced by the slug of each preset. - * For example: - * 'classes' => array( - * '.has-$slug-color' => 'color', - * '.has-$slug-background-color' => 'background-color', - * '.has-$slug-border-color' => 'border-color', - * ) - * - properties => array of CSS properties to be used by kses to - * validate the content of each preset - * by means of the remove_insecure_properties method. + * @return string[] */ - const PRESETS_METADATA = array( - array( - 'path' => array( 'color', 'palette' ), - 'prevent_override' => array( 'color', 'defaultPalette' ), - 'use_default_names' => false, - 'value_key' => 'color', - 'css_vars' => '--wp--preset--color--$slug', - 'classes' => array( - '.has-$slug-color' => 'color', - '.has-$slug-background-color' => 'background-color', - '.has-$slug-border-color' => 'border-color', - ), - 'properties' => array( 'color', 'background-color', 'border-color' ), - ), - array( - 'path' => array( 'color', 'gradients' ), - 'prevent_override' => array( 'color', 'defaultGradients' ), - 'use_default_names' => false, - 'value_key' => 'gradient', - 'css_vars' => '--wp--preset--gradient--$slug', - 'classes' => array( '.has-$slug-gradient-background' => 'background' ), - 'properties' => array( 'background' ), - ), - array( - 'path' => array( 'color', 'duotone' ), - 'prevent_override' => array( 'color', 'defaultDuotone' ), - 'use_default_names' => false, - 'value_func' => 'gutenberg_get_duotone_filter_property', - 'css_vars' => '--wp--preset--duotone--$slug', - 'classes' => array(), - 'properties' => array( 'filter' ), - ), - array( - 'path' => array( 'typography', 'fontSizes' ), - 'prevent_override' => false, - 'use_default_names' => true, - 'value_func' => 'gutenberg_get_typography_font_size_value', - 'css_vars' => '--wp--preset--font-size--$slug', - 'classes' => array( '.has-$slug-font-size' => 'font-size' ), - 'properties' => array( 'font-size' ), - ), - array( - 'path' => array( 'typography', 'fontFamilies' ), - 'prevent_override' => false, - 'use_default_names' => false, - 'value_key' => 'fontFamily', - 'css_vars' => '--wp--preset--font-family--$slug', - 'classes' => array( '.has-$slug-font-family' => 'font-family' ), - 'properties' => array( 'font-family' ), - ), - array( - 'path' => array( 'spacing', 'spacingSizes' ), - 'prevent_override' => false, - 'use_default_names' => true, - 'value_key' => 'size', - 'css_vars' => '--wp--preset--spacing--$slug', - 'classes' => array(), - 'properties' => array( 'padding', 'margin' ), - ), - ); + public function get_patterns() { + if ( isset( $this->theme_json['patterns'] ) && is_array( $this->theme_json['patterns'] ) ) { + return $this->theme_json['patterns']; + } + return array(); + } /** - * The valid properties under the settings key. + * Returns a valid theme.json as provided by a theme. * - * @var array + * Unlike get_raw_data() this returns the presets flattened, as provided by a theme. + * This also uses appearanceTools instead of their opt-ins if all of them are true. + * + * @since 6.0.0 + * + * @return array */ - const VALID_SETTINGS = array( - 'appearanceTools' => null, - 'useRootPaddingAwareAlignments' => null, - 'border' => array( - 'color' => null, - 'radius' => null, - 'style' => null, - 'width' => null, - ), - 'color' => array( - 'background' => null, - 'custom' => null, - 'customDuotone' => null, - 'customGradient' => null, - 'defaultDuotone' => null, - 'defaultGradients' => null, - 'defaultPalette' => null, - 'duotone' => null, - 'gradients' => null, - 'link' => null, - 'palette' => null, - 'text' => null, - ), - 'custom' => null, - 'layout' => array( - 'contentSize' => null, - 'definitions' => null, - 'wideSize' => null, - ), - 'spacing' => array( - 'customSpacingSize' => null, - 'spacingSizes' => null, - 'spacingScale' => null, - 'blockGap' => null, - 'margin' => null, - 'padding' => null, - 'units' => null, - ), - 'typography' => array( - 'fluid' => null, - 'customFontSize' => null, - 'dropCap' => null, - 'fontFamilies' => null, - 'fontSizes' => null, - 'fontStyle' => null, - 'fontWeight' => null, - 'letterSpacing' => null, - 'lineHeight' => null, - 'textDecoration' => null, - 'textTransform' => null, - ), - ); + public function get_data() { + $output = $this->theme_json; + $nodes = static::get_setting_nodes( $output ); + + /** + * Flatten the theme & custom origins into a single one. + * + * For example, the following: + * + * { + * "settings": { + * "color": { + * "palette": { + * "theme": [ {} ], + * "custom": [ {} ] + * } + * } + * } + * } + * + * will be converted to: + * + * { + * "settings": { + * "color": { + * "palette": [ {} ] + * } + * } + * } + */ + foreach ( $nodes as $node ) { + foreach ( static::PRESETS_METADATA as $preset_metadata ) { + $path = $node['path']; + foreach ( $preset_metadata['path'] as $preset_metadata_path ) { + $path[] = $preset_metadata_path; + } + $preset = _wp_array_get( $output, $path, null ); + if ( null === $preset ) { + continue; + } + + $items = array(); + if ( isset( $preset['theme'] ) ) { + foreach ( $preset['theme'] as $item ) { + $slug = $item['slug']; + unset( $item['slug'] ); + $items[ $slug ] = $item; + } + } + if ( isset( $preset['custom'] ) ) { + foreach ( $preset['custom'] as $item ) { + $slug = $item['slug']; + unset( $item['slug'] ); + $items[ $slug ] = $item; + } + } + $flattened_preset = array(); + foreach ( $items as $slug => $value ) { + $flattened_preset[] = array_merge( array( 'slug' => (string) $slug ), $value ); + } + _wp_array_set( $output, $path, $flattened_preset ); + } + } + + // If all of the static::APPEARANCE_TOOLS_OPT_INS are true, + // this code unsets them and sets 'appearanceTools' instead. + foreach ( $nodes as $node ) { + $all_opt_ins_are_set = true; + foreach ( static::APPEARANCE_TOOLS_OPT_INS as $opt_in_path ) { + $full_path = $node['path']; + foreach ( $opt_in_path as $opt_in_path_item ) { + $full_path[] = $opt_in_path_item; + } + // Use "unset prop" as a marker instead of "null" because + // "null" can be a valid value for some props (e.g. blockGap). + $opt_in_value = _wp_array_get( $output, $full_path, 'unset prop' ); + if ( 'unset prop' === $opt_in_value ) { + $all_opt_ins_are_set = false; + break; + } + } + + if ( $all_opt_ins_are_set ) { + $node_path_with_appearance_tools = $node['path']; + $node_path_with_appearance_tools[] = 'appearanceTools'; + _wp_array_set( $output, $node_path_with_appearance_tools, true ); + foreach ( static::APPEARANCE_TOOLS_OPT_INS as $opt_in_path ) { + $full_path = $node['path']; + foreach ( $opt_in_path as $opt_in_path_item ) { + $full_path[] = $opt_in_path_item; + } + // Use "unset prop" as a marker instead of "null" because + // "null" can be a valid value for some props (e.g. blockGap). + $opt_in_value = _wp_array_get( $output, $full_path, 'unset prop' ); + if ( true !== $opt_in_value ) { + continue; + } + + // The following could be improved to be path independent. + // At the moment it relies on a couple of assumptions: + // + // - all opt-ins having a path of size 2. + // - there's two sources of settings: the top-level and the block-level. + if ( + ( 1 === count( $node['path'] ) ) && + ( 'settings' === $node['path'][0] ) + ) { + // Top-level settings. + unset( $output['settings'][ $opt_in_path[0] ][ $opt_in_path[1] ] ); + if ( empty( $output['settings'][ $opt_in_path[0] ] ) ) { + unset( $output['settings'][ $opt_in_path[0] ] ); + } + } elseif ( + ( 3 === count( $node['path'] ) ) && + ( 'settings' === $node['path'][0] ) && + ( 'blocks' === $node['path'][1] ) + ) { + // Block-level settings. + $block_name = $node['path'][2]; + unset( $output['settings']['blocks'][ $block_name ][ $opt_in_path[0] ][ $opt_in_path[1] ] ); + if ( empty( $output['settings']['blocks'][ $block_name ][ $opt_in_path[0] ] ) ) { + unset( $output['settings']['blocks'][ $block_name ][ $opt_in_path[0] ] ); + } + } + } + } + } + + wp_recursive_ksort( $output ); + + return $output; + } /** - * Transform the spacing scale values into an array of spacing scale presets. + * Sets the spacingSizes array based on the spacingScale values from theme.json. + * + * @since 6.1.0 + * + * @return null|void */ public function set_spacing_sizes() { $spacing_scale = _wp_array_get( $this->theme_json, array( 'settings', 'spacing', 'spacingScale' ), array() ); - if ( ! is_numeric( $spacing_scale['steps'] ) + // Gutenberg didn't have the 1st isset check. + if ( ! isset( $spacing_scale['steps'] ) + || ! is_numeric( $spacing_scale['steps'] ) || ! isset( $spacing_scale['mediumStep'] ) || ! isset( $spacing_scale['unit'] ) || ! isset( $spacing_scale['operator'] ) @@ -1410,7 +3197,7 @@ public function set_spacing_sizes() { $below_sizes[] = array( /* translators: %s: Digit to indicate multiple of sizing, eg. 2X-Small. */ - 'name' => $below_midpoint_count === $steps_mid_point - 1 ? __( 'Small', 'gutenberg' ) : sprintf( __( '%sX-Small', 'gutenberg' ), strval( $x_small_count ) ), + 'name' => $below_midpoint_count === $steps_mid_point - 1 ? __( 'Small', 'gutenberg' ) : sprintf( __( '%sX-Small', 'gutenberg' ), (string) $x_small_count ), 'slug' => (string) $slug, 'size' => round( $current_step, 2 ) . $unit, ); @@ -1420,7 +3207,7 @@ public function set_spacing_sizes() { } if ( $below_midpoint_count < $steps_mid_point - 2 ) { - ++$x_small_count; + $x_small_count++; } $slug -= 10; @@ -1447,7 +3234,7 @@ public function set_spacing_sizes() { $above_sizes[] = array( /* translators: %s: Digit to indicate multiple of sizing, eg. 2X-Large. */ - 'name' => 0 === $above_midpoint_count ? __( 'Large', 'gutenberg' ) : sprintf( __( '%sX-Large', 'gutenberg' ), strval( $x_large_count ) ), + 'name' => 0 === $above_midpoint_count ? __( 'Large', 'gutenberg' ) : sprintf( __( '%sX-Large', 'gutenberg' ), (string) $x_large_count ), 'slug' => (string) $slug, 'size' => round( $current_step, 2 ) . $unit, ); @@ -1457,244 +3244,24 @@ public function set_spacing_sizes() { } if ( $above_midpoint_count > 1 ) { - ++$x_large_count; + $x_large_count++; } $slug += 10; } - $spacing_sizes = array_merge( $below_sizes, $above_sizes ); + $spacing_sizes = $below_sizes; + foreach ( $above_sizes as $above_sizes_item ) { + $spacing_sizes[] = $above_sizes_item; + } // If there are 7 or less steps in the scale revert to numbers for labels instead of t-shirt sizes. if ( $spacing_scale['steps'] <= 7 ) { for ( $spacing_sizes_count = 0; $spacing_sizes_count < count( $spacing_sizes ); $spacing_sizes_count++ ) { - $spacing_sizes[ $spacing_sizes_count ]['name'] = strval( $spacing_sizes_count + 1 ); + $spacing_sizes[ $spacing_sizes_count ]['name'] = (string) ( $spacing_sizes_count + 1 ); } } _wp_array_set( $this->theme_json, array( 'settings', 'spacing', 'spacingSizes', 'default' ), $spacing_sizes ); } - - /** - * Get the CSS layout rules for a particular block from theme.json layout definitions. - * - * @param array $block_metadata Metadata about the block to get styles for. - * - * @return string Layout styles for the block. - */ - protected function get_layout_styles( $block_metadata ) { - $block_rules = ''; - $block_type = null; - - // Skip outputting layout styles if explicitly disabled. - if ( current_theme_supports( 'disable-layout-styles' ) ) { - return $block_rules; - } - - if ( isset( $block_metadata['name'] ) ) { - $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block_metadata['name'] ); - if ( ! block_has_support( $block_type, array( '__experimentalLayout' ), false ) ) { - return $block_rules; - } - } - - $selector = isset( $block_metadata['selector'] ) ? $block_metadata['selector'] : ''; - $has_block_gap_support = _wp_array_get( $this->theme_json, array( 'settings', 'spacing', 'blockGap' ) ) !== null; - $has_fallback_gap_support = ! $has_block_gap_support; // This setting isn't useful yet: it exists as a placeholder for a future explicit fallback gap styles support. - $node = _wp_array_get( $this->theme_json, $block_metadata['path'], array() ); - $layout_definitions = _wp_array_get( $this->theme_json, array( 'settings', 'layout', 'definitions' ), array() ); - $layout_selector_pattern = '/^[a-zA-Z0-9\-\.\ *+>:\(\)]*$/'; // Allow alphanumeric classnames, spaces, wildcard, sibling, child combinator and pseudo class selectors. - - // Gap styles will only be output if the theme has block gap support, or supports a fallback gap. - // Default layout gap styles will be skipped for themes that do not explicitly opt-in to blockGap with a `true` or `false` value. - if ( $has_block_gap_support || $has_fallback_gap_support ) { - $block_gap_value = null; - // Use a fallback gap value if block gap support is not available. - if ( ! $has_block_gap_support ) { - $block_gap_value = static::ROOT_BLOCK_SELECTOR === $selector ? '0.5em' : null; - if ( ! empty( $block_type ) ) { - $block_gap_value = _wp_array_get( $block_type->supports, array( 'spacing', 'blockGap', '__experimentalDefault' ), null ); - } - } else { - $block_gap_value = static::get_property_value( $node, array( 'spacing', 'blockGap' ) ); - } - - // Support split row / column values and concatenate to a shorthand value. - if ( is_array( $block_gap_value ) ) { - if ( isset( $block_gap_value['top'] ) && isset( $block_gap_value['left'] ) ) { - $gap_row = static::get_property_value( $node, array( 'spacing', 'blockGap', 'top' ) ); - $gap_column = static::get_property_value( $node, array( 'spacing', 'blockGap', 'left' ) ); - $block_gap_value = $gap_row === $gap_column ? $gap_row : $gap_row . ' ' . $gap_column; - } else { - // Skip outputting gap value if not all sides are provided. - $block_gap_value = null; - } - } - - // If the block should have custom gap, add the gap styles. - if ( null !== $block_gap_value && false !== $block_gap_value && '' !== $block_gap_value ) { - foreach ( $layout_definitions as $layout_definition_key => $layout_definition ) { - // Allow outputting fallback gap styles for flex layout type when block gap support isn't available. - if ( ! $has_block_gap_support && 'flex' !== $layout_definition_key ) { - continue; - } - - $class_name = sanitize_title( _wp_array_get( $layout_definition, array( 'className' ), false ) ); - $spacing_rules = _wp_array_get( $layout_definition, array( 'spacingStyles' ), array() ); - - if ( - ! empty( $class_name ) && - ! empty( $spacing_rules ) - ) { - foreach ( $spacing_rules as $spacing_rule ) { - $declarations = array(); - if ( - isset( $spacing_rule['selector'] ) && - preg_match( $layout_selector_pattern, $spacing_rule['selector'] ) && - ! empty( $spacing_rule['rules'] ) - ) { - // Iterate over each of the styling rules and substitute non-string values such as `null` with the real `blockGap` value. - foreach ( $spacing_rule['rules'] as $css_property => $css_value ) { - $current_css_value = is_string( $css_value ) ? $css_value : $block_gap_value; - if ( static::is_safe_css_declaration( $css_property, $current_css_value ) ) { - $declarations[] = array( - 'name' => $css_property, - 'value' => $current_css_value, - ); - } - } - - if ( ! $has_block_gap_support ) { - // For fallback gap styles, use lower specificity, to ensure styles do not unintentionally override theme styles. - $format = static::ROOT_BLOCK_SELECTOR === $selector ? ':where(.%2$s%3$s)' : ':where(%1$s.%2$s%3$s)'; - $layout_selector = sprintf( - $format, - $selector, - $class_name, - $spacing_rule['selector'] - ); - } else { - $format = static::ROOT_BLOCK_SELECTOR === $selector ? '%s .%s%s' : '%s.%s%s'; - $layout_selector = sprintf( - $format, - $selector, - $class_name, - $spacing_rule['selector'] - ); - } - $block_rules .= static::to_ruleset( $layout_selector, $declarations ); - } - } - } - } - } - } - - // Output base styles. - if ( - static::ROOT_BLOCK_SELECTOR === $selector - ) { - $valid_display_modes = array( 'block', 'flex', 'grid' ); - foreach ( $layout_definitions as $layout_definition ) { - $class_name = sanitize_title( _wp_array_get( $layout_definition, array( 'className' ), false ) ); - $base_style_rules = _wp_array_get( $layout_definition, array( 'baseStyles' ), array() ); - - if ( - ! empty( $class_name ) && - ! empty( $base_style_rules ) - ) { - // Output display mode. This requires special handling as `display` is not exposed in `safe_style_css_filter`. - if ( - ! empty( $layout_definition['displayMode'] ) && - is_string( $layout_definition['displayMode'] ) && - in_array( $layout_definition['displayMode'], $valid_display_modes, true ) - ) { - $layout_selector = sprintf( - '%s .%s', - $selector, - $class_name - ); - $block_rules .= static::to_ruleset( - $layout_selector, - array( - array( - 'name' => 'display', - 'value' => $layout_definition['displayMode'], - ), - ) - ); - } - - foreach ( $base_style_rules as $base_style_rule ) { - $declarations = array(); - - if ( - isset( $base_style_rule['selector'] ) && - preg_match( $layout_selector_pattern, $base_style_rule['selector'] ) && - ! empty( $base_style_rule['rules'] ) - ) { - foreach ( $base_style_rule['rules'] as $css_property => $css_value ) { - if ( static::is_safe_css_declaration( $css_property, $css_value ) ) { - $declarations[] = array( - 'name' => $css_property, - 'value' => $css_value, - ); - } - } - - $layout_selector = sprintf( - '%s .%s%s', - $selector, - $class_name, - $base_style_rule['selector'] - ); - $block_rules .= static::to_ruleset( $layout_selector, $declarations ); - } - } - } - } - } - return $block_rules; - } - - /** - * Function that scopes a selector with another one. This works a bit like - * SCSS nesting except the `&` operator isn't supported. - * - * - * $scope = '.a, .b .c'; - * $selector = '> .x, .y'; - * $merged = scope_selector( $scope, $selector ); - * // $merged is '.a > .x, .a .y, .b .c > .x, .b .c .y' - * - * - * @since 5.9.0 - * - * @param string $scope Selector to scope to. - * @param string $selector Original selector. - * @return string Scoped selector. - */ - public static function scope_selector( $scope, $selector ) { - $scopes = explode( ',', $scope ); - $selectors = explode( ',', $selector ); - - $selectors_scoped = array(); - foreach ( $scopes as $outer ) { - foreach ( $selectors as $inner ) { - $outer = trim( $outer ); - $inner = trim( $inner ); - if ( ! empty( $outer ) && ! empty( $inner ) ) { - $selectors_scoped[] = $outer . ' ' . $inner; - } elseif ( empty( $outer ) ) { - $selectors_scoped[] = $inner; - } elseif ( empty( $inner ) ) { - $selectors_scoped[] = $outer; - } - } - } - - $result = implode( ', ', $selectors_scoped ); - return $result; - } - } 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 deleted file mode 100644 index fe01eb273066e2..00000000000000 --- a/lib/compat/wordpress-6.2/class-wp-theme-json-6-2.php +++ /dev/null @@ -1,391 +0,0 @@ - array( 'color', 'gradient' ), - 'background-color' => array( 'color', 'background' ), - 'border-radius' => array( 'border', 'radius' ), - 'border-top-left-radius' => array( 'border', 'radius', 'topLeft' ), - 'border-top-right-radius' => array( 'border', 'radius', 'topRight' ), - 'border-bottom-left-radius' => array( 'border', 'radius', 'bottomLeft' ), - 'border-bottom-right-radius' => array( 'border', 'radius', 'bottomRight' ), - 'border-color' => array( 'border', 'color' ), - 'border-width' => array( 'border', 'width' ), - 'border-style' => array( 'border', 'style' ), - 'border-top-color' => array( 'border', 'top', 'color' ), - 'border-top-width' => array( 'border', 'top', 'width' ), - 'border-top-style' => array( 'border', 'top', 'style' ), - 'border-right-color' => array( 'border', 'right', 'color' ), - 'border-right-width' => array( 'border', 'right', 'width' ), - 'border-right-style' => array( 'border', 'right', 'style' ), - 'border-bottom-color' => array( 'border', 'bottom', 'color' ), - 'border-bottom-width' => array( 'border', 'bottom', 'width' ), - 'border-bottom-style' => array( 'border', 'bottom', 'style' ), - 'border-left-color' => array( 'border', 'left', 'color' ), - 'border-left-width' => array( 'border', 'left', 'width' ), - 'border-left-style' => array( 'border', 'left', 'style' ), - 'color' => array( 'color', 'text' ), - 'font-family' => array( 'typography', 'fontFamily' ), - 'font-size' => array( 'typography', 'fontSize' ), - 'font-style' => array( 'typography', 'fontStyle' ), - 'font-weight' => array( 'typography', 'fontWeight' ), - 'letter-spacing' => array( 'typography', 'letterSpacing' ), - 'line-height' => array( 'typography', 'lineHeight' ), - 'margin' => array( 'spacing', 'margin' ), - 'margin-top' => array( 'spacing', 'margin', 'top' ), - 'margin-right' => array( 'spacing', 'margin', 'right' ), - 'margin-bottom' => array( 'spacing', 'margin', 'bottom' ), - 'margin-left' => array( 'spacing', 'margin', 'left' ), - 'min-height' => array( 'dimensions', 'minHeight' ), - 'padding' => array( 'spacing', 'padding' ), - 'padding-top' => array( 'spacing', 'padding', 'top' ), - 'padding-right' => array( 'spacing', 'padding', 'right' ), - 'padding-bottom' => array( 'spacing', 'padding', 'bottom' ), - 'padding-left' => array( 'spacing', 'padding', 'left' ), - '--wp--style--root--padding' => array( 'spacing', 'padding' ), - '--wp--style--root--padding-top' => array( 'spacing', 'padding', 'top' ), - '--wp--style--root--padding-right' => array( 'spacing', 'padding', 'right' ), - '--wp--style--root--padding-bottom' => array( 'spacing', 'padding', 'bottom' ), - '--wp--style--root--padding-left' => array( 'spacing', 'padding', 'left' ), - 'text-decoration' => array( 'typography', 'textDecoration' ), - 'text-transform' => array( 'typography', 'textTransform' ), - 'filter' => array( 'filter', 'duotone' ), - '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. - * - * @since 5.8.0 As `ALLOWED_SETTINGS`. - * @since 5.9.0 Renamed from `ALLOWED_SETTINGS` to `VALID_SETTINGS`, - * added new properties for `border`, `color`, `spacing`, - * and `typography`, and renamed others according to the new schema. - * @since 6.0.0 Added `color.defaultDuotone`. - * @since 6.1.0 Added `layout.definitions` and `useRootPaddingAwareAlignments`. - * @since 6.2.0 Added `dimensions.minHeight`. - * @var array - */ - const VALID_SETTINGS = array( - 'appearanceTools' => null, - 'useRootPaddingAwareAlignments' => null, - 'border' => array( - 'color' => null, - 'radius' => null, - 'style' => null, - 'width' => null, - ), - 'color' => array( - 'background' => null, - 'custom' => null, - 'customDuotone' => null, - 'customGradient' => null, - 'defaultDuotone' => null, - 'defaultGradients' => null, - 'defaultPalette' => null, - 'duotone' => null, - 'gradients' => null, - 'link' => null, - 'palette' => null, - 'text' => null, - ), - 'custom' => null, - 'dimensions' => array( - 'minHeight' => null, - ), - 'layout' => array( - 'contentSize' => null, - 'definitions' => null, - 'wideSize' => null, - ), - 'spacing' => array( - 'customSpacingSize' => null, - 'spacingSizes' => null, - 'spacingScale' => null, - 'blockGap' => null, - 'margin' => null, - 'padding' => null, - 'units' => null, - ), - 'typography' => array( - 'fluid' => null, - 'customFontSize' => null, - 'dropCap' => null, - 'fontFamilies' => null, - 'fontSizes' => null, - 'fontStyle' => null, - 'fontWeight' => null, - 'letterSpacing' => null, - 'lineHeight' => null, - 'textDecoration' => null, - 'textTransform' => null, - ), - ); - - /** - * The valid properties under the styles key. - * - * @since 5.8.0 As `ALLOWED_STYLES`. - * @since 5.9.0 Renamed from `ALLOWED_STYLES` to `VALID_STYLES`, - * added new properties for `border`, `filter`, `spacing`, - * and `typography`. - * @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( - 'border' => array( - 'color' => null, - 'radius' => null, - 'style' => null, - 'width' => null, - 'top' => null, - 'right' => null, - 'bottom' => null, - 'left' => null, - ), - 'color' => array( - 'background' => null, - 'gradient' => null, - 'text' => null, - ), - 'dimensions' => array( - 'minHeight' => null, - ), - 'filter' => array( - 'duotone' => null, - ), - 'shadow' => null, - 'spacing' => array( - 'margin' => null, - 'padding' => null, - 'blockGap' => null, - ), - 'typography' => array( - 'fontFamily' => null, - 'fontSize' => null, - 'fontStyle' => null, - 'fontWeight' => null, - 'letterSpacing' => null, - 'lineHeight' => null, - '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/experimental/class-wp-theme-json-gutenberg.php b/lib/experimental/class-wp-theme-json-gutenberg.php deleted file mode 100644 index a16697dff07d07..00000000000000 --- a/lib/experimental/class-wp-theme-json-gutenberg.php +++ /dev/null @@ -1,31 +0,0 @@ - Date: Fri, 16 Dec 2022 15:47:22 +0400 Subject: [PATCH 37/51] Migrate Font Size Picker tests to Playwright (#46591) * Migrate Font Size Picker tests to Playwright * Remove legacy tests * Open document settings before each test --- .../editor/various/font-size-picker.test.js | 314 ------------------ .../editor/various/font-size-picker.spec.js | 305 +++++++++++++++++ 2 files changed, 305 insertions(+), 314 deletions(-) delete mode 100644 packages/e2e-tests/specs/editor/various/font-size-picker.test.js create mode 100644 test/e2e/specs/editor/various/font-size-picker.spec.js diff --git a/packages/e2e-tests/specs/editor/various/font-size-picker.test.js b/packages/e2e-tests/specs/editor/various/font-size-picker.test.js deleted file mode 100644 index f7351450dabd51..00000000000000 --- a/packages/e2e-tests/specs/editor/various/font-size-picker.test.js +++ /dev/null @@ -1,314 +0,0 @@ -/** - * WordPress dependencies - */ -import { - clickBlockAppender, - getEditedPostContent, - createNewPost, - pressKeyWithModifier, - pressKeyTimes, - openTypographyToolsPanelMenu, -} from '@wordpress/e2e-test-utils'; - -const openFontSizeSelectControl = async () => { - const selectControlSelector = - "//div[contains(@class, 'components-font-size-picker__controls')]//button[contains(@class, 'components-custom-select-control__button')]"; - const selectControl = await page.waitForXPath( selectControlSelector ); - return selectControl.click(); -}; - -const FONT_SIZE_TOGGLE_GROUP_SELECTOR = - "//div[contains(@class, 'components-font-size-picker__controls')]//div[contains(@class, 'components-toggle-group-control')]"; - -// Click a button by its label - applies when ToggleGroupControl is used. -const clickFontSizeButtonByLabel = async ( label ) => { - const buttonSelector = `${ FONT_SIZE_TOGGLE_GROUP_SELECTOR }//button[@aria-label='${ label }']`; - const button = await page.waitForXPath( buttonSelector ); - return button.click(); -}; - -// Clicks the button to toggle between custom size input and the control for the presets. -const toggleCustomInput = async ( showCustomInput ) => { - const label = showCustomInput ? 'Set custom size' : 'Use size preset'; - const toggleButton = await page.waitForXPath( - `//button[@aria-label='${ label }']` - ); - return toggleButton.click(); -}; - -const clickCustomInput = async () => { - const customInput = await page.waitForXPath( "//input[@type='number']" ); - return customInput.click(); -}; - -describe( 'Font Size Picker', () => { - beforeEach( async () => { - await createNewPost(); - } ); - describe( 'Common', () => { - it( 'should apply a named font size using the font size input', async () => { - // Create a paragraph block with some content. - await clickBlockAppender(); - await page.keyboard.type( 'Paragraph to be made "small"' ); - - await toggleCustomInput( true ); - await clickCustomInput(); - // This should be the "small" font-size of the editor defaults. - await page.keyboard.type( '13' ); - expect( await getEditedPostContent() ).toMatchInlineSnapshot( ` - " -

Paragraph to be made \\"small\\"

- " - ` ); - } ); - it( 'should apply a custom font size using the font size input', async () => { - // Create a paragraph block with some content. - await clickBlockAppender(); - await page.keyboard.type( 'Paragraph to be made "small"' ); - - await toggleCustomInput( true ); - await clickCustomInput(); - await page.keyboard.type( '23' ); - expect( await getEditedPostContent() ).toMatchInlineSnapshot( ` - " -

Paragraph to be made \\"small\\"

- " - ` ); - } ); - it( 'should reset a custom font size using input field', async () => { - // Create a paragraph block with some content. - await clickBlockAppender(); - await page.keyboard.type( 'Paragraph reset - custom size' ); - - await toggleCustomInput( true ); - await clickCustomInput(); - await page.keyboard.type( '23' ); - expect( await getEditedPostContent() ).toMatchInlineSnapshot( ` - " -

Paragraph reset - custom size

- " - ` ); - - await pressKeyTimes( 'Backspace', 2 ); - expect( await getEditedPostContent() ).toMatchInlineSnapshot( ` - " -

Paragraph reset - custom size

- " - ` ); - } ); - } ); - - // A different control is rendered based on the available font sizes number. - describe( 'More font sizes', () => { - beforeEach( async () => { - await page.evaluate( () => { - // set a deep `path[]` property in object `obj` to `value`, immutably - function setDeep( obj, path, value ) { - function doSet( o, i ) { - if ( i < path.length ) { - const key = path[ i ]; - return { ...o, [ key ]: doSet( o[ key ], i + 1 ) }; - } - return value; - } - return doSet( obj, 0 ); - } - - wp.data.dispatch( 'core/block-editor' ).updateSettings( - setDeep( - wp.data.select( 'core/block-editor' ).getSettings(), - [ - '__experimentalFeatures', - 'typography', - 'fontSizes', - 'default', - ], - [ - { - name: 'Tiny', - slug: 'tiny', - size: '11px', - }, - { - name: 'Small', - slug: 'small', - size: '13px', - }, - { - name: 'Medium', - slug: 'medium', - size: '20px', - }, - { - name: 'Large', - slug: 'large', - size: '36px', - }, - { - name: 'Extra Large', - slug: 'x-large', - size: '42px', - }, - { - name: 'Huge', - slug: 'huge', - size: '48px', - }, - ] - ) - ); - } ); - } ); - - it( 'should apply a named font size using the font size buttons', async () => { - // Create a paragraph block with some content. - await clickBlockAppender(); - await page.keyboard.type( 'Paragraph to be made "large"' ); - - await openFontSizeSelectControl(); - await pressKeyTimes( 'ArrowDown', 4 ); - await page.keyboard.press( 'Enter' ); - - expect( await getEditedPostContent() ).toMatchInlineSnapshot( ` - " -

Paragraph to be made \\"large\\"

- " - ` ); - } ); - it( 'should reset a named font size using the tools panel menu', async () => { - // Create a paragraph block with some content. - await clickBlockAppender(); - await page.keyboard.type( - 'Paragraph with font size reset using tools panel menu' - ); - - await openFontSizeSelectControl(); - await pressKeyTimes( 'ArrowDown', 3 ); - await page.keyboard.press( 'Enter' ); - expect( await getEditedPostContent() ).toMatchInlineSnapshot( ` - " -

Paragraph with font size reset using tools panel menu

- " - ` ); - - // Open Typography ToolsPanel, font size will be first in menu and gain focus. - await openTypographyToolsPanelMenu(); - - await page.keyboard.press( 'Enter' ); - expect( await getEditedPostContent() ).toMatchInlineSnapshot( ` - " -

Paragraph with font size reset using tools panel menu

- " - ` ); - } ); - - it( 'should reset a named font size using input field', async () => { - // Create a paragraph block with some content. - await clickBlockAppender(); - await page.keyboard.type( - 'Paragraph with font size reset using input field' - ); - - await openFontSizeSelectControl(); - await pressKeyTimes( 'ArrowDown', 2 ); - await page.keyboard.press( 'Enter' ); - expect( await getEditedPostContent() ).toMatchInlineSnapshot( ` - " -

Paragraph with font size reset using input field

- " - ` ); - - await toggleCustomInput( true ); - await clickCustomInput(); - await pressKeyWithModifier( 'primary', 'A' ); - await page.keyboard.press( 'Backspace' ); - - // Disable reason: Wait for changes to apply. - // eslint-disable-next-line no-restricted-syntax - await page.waitForTimeout( 1000 ); - - expect( await getEditedPostContent() ).toMatchInlineSnapshot( ` - " -

Paragraph with font size reset using input field

- " - ` ); - } ); - } ); - describe( 'Few font sizes', () => { - it( 'should apply a named font size using the font size buttons', async () => { - // Create a paragraph block with some content. - await clickBlockAppender(); - await page.keyboard.type( 'Paragraph to be made "large"' ); - - await clickFontSizeButtonByLabel( 'Large' ); - const buttonSelector = `${ FONT_SIZE_TOGGLE_GROUP_SELECTOR }//button[@aria-checked='true']`; - const [ activeButton ] = await page.$x( buttonSelector ); - const activeLabel = await page.evaluate( - ( element ) => element?.getAttribute( 'aria-label' ), - activeButton - ); - expect( activeLabel ).toEqual( 'Large' ); - - expect( await getEditedPostContent() ).toMatchInlineSnapshot( ` - " -

Paragraph to be made \\"large\\"

- " - ` ); - } ); - - it( 'should reset a named font size using the tools panel menu', async () => { - // Create a paragraph block with some content. - await clickBlockAppender(); - await page.keyboard.type( - 'Paragraph with font size reset using tools panel menu' - ); - - await clickFontSizeButtonByLabel( 'Small' ); - expect( await getEditedPostContent() ).toMatchInlineSnapshot( ` - " -

Paragraph with font size reset using tools panel menu

- " - ` ); - - // Open Typography ToolsPanel, font size will be first in menu and gain focus. - await openTypographyToolsPanelMenu(); - - await page.keyboard.press( 'Enter' ); - expect( await getEditedPostContent() ).toMatchInlineSnapshot( ` - " -

Paragraph with font size reset using tools panel menu

- " - ` ); - } ); - - it( 'should reset a named font size using input field', async () => { - // Create a paragraph block with some content. - await clickBlockAppender(); - await page.keyboard.type( - 'Paragraph with font size reset using input field' - ); - - await clickFontSizeButtonByLabel( 'Small' ); - expect( await getEditedPostContent() ).toMatchInlineSnapshot( ` - " -

Paragraph with font size reset using input field

- " - ` ); - - await toggleCustomInput( true ); - await clickCustomInput(); - await pressKeyWithModifier( 'primary', 'A' ); - await page.keyboard.press( 'Backspace' ); - - // Disable reason: Wait for changes to apply. - // eslint-disable-next-line no-restricted-syntax - await page.waitForTimeout( 1000 ); - - expect( await getEditedPostContent() ).toMatchInlineSnapshot( ` - " -

Paragraph with font size reset using input field

- " - ` ); - } ); - } ); -} ); diff --git a/test/e2e/specs/editor/various/font-size-picker.spec.js b/test/e2e/specs/editor/various/font-size-picker.spec.js new file mode 100644 index 00000000000000..38707c12b261bc --- /dev/null +++ b/test/e2e/specs/editor/various/font-size-picker.spec.js @@ -0,0 +1,305 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +test.describe( 'Font Size Picker', () => { + test.beforeEach( async ( { admin } ) => { + await admin.createNewPost(); + } ); + + test.describe( 'Common', () => { + test( 'should apply a named font size using the font size input', async ( { + editor, + page, + } ) => { + await editor.openDocumentSettingsSidebar(); + await page.click( 'role=button[name="Add default block"i]' ); + await page.keyboard.type( 'Paragraph to be made "small"' ); + await page.click( + 'role=region[name="Editor settings"i] >> role=button[name="Set custom size"i]' + ); + await page.click( 'role=spinbutton[name="Custom"i]' ); + + // This should be the "small" font-size of the editor defaults. + await page.keyboard.type( '13' ); + + await expect.poll( editor.getEditedPostContent ) + .toBe( ` +

Paragraph to be made "small"

+` ); + } ); + + test( 'should apply a custom font size using the font size input', async ( { + editor, + page, + } ) => { + await editor.openDocumentSettingsSidebar(); + await page.click( 'role=button[name="Add default block"i]' ); + await page.keyboard.type( 'Paragraph to be made "small"' ); + await page.click( + 'role=region[name="Editor settings"i] >> role=button[name="Set custom size"i]' + ); + await page.click( 'role=spinbutton[name="Custom"i]' ); + + await page.keyboard.type( '23' ); + + await expect.poll( editor.getEditedPostContent ) + .toBe( ` +

Paragraph to be made "small"

+` ); + } ); + + test( 'should reset a custom font size using input field', async ( { + editor, + page, + pageUtils, + } ) => { + await editor.openDocumentSettingsSidebar(); + await page.click( 'role=button[name="Add default block"i]' ); + await page.keyboard.type( 'Paragraph reset - custom size' ); + await page.click( + 'role=region[name="Editor settings"i] >> role=button[name="Set custom size"i]' + ); + await page.click( 'role=spinbutton[name="Custom"i]' ); + await page.keyboard.type( '23' ); + + await expect.poll( editor.getEditedPostContent ) + .toBe( ` +

Paragraph reset - custom size

+` ); + + await pageUtils.pressKeyTimes( 'Backspace', 2 ); + await expect.poll( editor.getEditedPostContent ) + .toBe( ` +

Paragraph reset - custom size

+` ); + } ); + } ); + + test.describe( 'More font sizes', () => { + test.beforeEach( async ( { page } ) => { + await page.evaluate( () => { + // set a deep `path[]` property in object `obj` to `value`, immutably + function setDeep( obj, path, value ) { + function doSet( o, i ) { + if ( i < path.length ) { + const key = path[ i ]; + return { ...o, [ key ]: doSet( o[ key ], i + 1 ) }; + } + return value; + } + return doSet( obj, 0 ); + } + + window.wp.data.dispatch( 'core/block-editor' ).updateSettings( + setDeep( + window.wp.data + .select( 'core/block-editor' ) + .getSettings(), + [ + '__experimentalFeatures', + 'typography', + 'fontSizes', + 'default', + ], + [ + { + name: 'Tiny', + slug: 'tiny', + size: '11px', + }, + { + name: 'Small', + slug: 'small', + size: '13px', + }, + { + name: 'Medium', + slug: 'medium', + size: '20px', + }, + { + name: 'Large', + slug: 'large', + size: '36px', + }, + { + name: 'Extra Large', + slug: 'x-large', + size: '42px', + }, + { + name: 'Huge', + slug: 'huge', + size: '48px', + }, + ] + ) + ); + } ); + } ); + + test( 'should apply a named font size using the font size buttons', async ( { + editor, + page, + pageUtils, + } ) => { + await editor.openDocumentSettingsSidebar(); + await page.click( 'role=button[name="Add default block"i]' ); + await page.keyboard.type( 'Paragraph to be made "large"' ); + await page.click( + 'role=group[name="Font size"i] >> role=button[name="Font size"i]' + ); + await pageUtils.pressKeyTimes( 'ArrowDown', 4 ); + await page.keyboard.press( 'Enter' ); + + await expect.poll( editor.getEditedPostContent ) + .toBe( ` +

Paragraph to be made "large"

+` ); + } ); + + test( 'should reset a named font size using the tools panel menu', async ( { + editor, + page, + pageUtils, + } ) => { + await editor.openDocumentSettingsSidebar(); + await page.click( 'role=button[name="Add default block"i]' ); + await page.keyboard.type( + 'Paragraph with font size reset using tools panel menu' + ); + await page.click( + 'role=group[name="Font size"i] >> role=button[name="Font size"i]' + ); + await pageUtils.pressKeyTimes( 'ArrowDown', 3 ); + await page.keyboard.press( 'Enter' ); + + await expect.poll( editor.getEditedPostContent ) + .toBe( ` +

Paragraph with font size reset using tools panel menu

+` ); + + await page.click( 'role=button[name="Typography options"i]' ); + await page.click( 'role=menuitem[name="Reset Font size"i]' ); + + await expect.poll( editor.getEditedPostContent ) + .toBe( ` +

Paragraph with font size reset using tools panel menu

+` ); + } ); + + test( 'should reset a named font size using input field', async ( { + editor, + page, + pageUtils, + } ) => { + await editor.openDocumentSettingsSidebar(); + await page.click( 'role=button[name="Add default block"i]' ); + await page.keyboard.type( + 'Paragraph with font size reset using input field' + ); + await page.click( + 'role=group[name="Font size"i] >> role=button[name="Font size"i]' + ); + await pageUtils.pressKeyTimes( 'ArrowDown', 2 ); + await page.keyboard.press( 'Enter' ); + + await expect.poll( editor.getEditedPostContent ) + .toBe( ` +

Paragraph with font size reset using input field

+` ); + + await page.click( + 'role=region[name="Editor settings"i] >> role=button[name="Set custom size"i]' + ); + await page.click( 'role=spinbutton[name="Custom"i]' ); + await pageUtils.pressKeyWithModifier( 'primary', 'A' ); + await page.keyboard.press( 'Backspace' ); + + await expect.poll( editor.getEditedPostContent ) + .toBe( ` +

Paragraph with font size reset using input field

+` ); + } ); + } ); + + test.describe( 'Few font sizes', () => { + test( 'should apply a named font size using the font size buttons', async ( { + editor, + page, + } ) => { + await editor.openDocumentSettingsSidebar(); + await page.click( 'role=button[name="Add default block"i]' ); + await page.keyboard.type( 'Paragraph to be made "large"' ); + await page.click( + 'role=radiogroup[name="Font size"i] >> role=radio[name="Large"i]' + ); + + await expect.poll( editor.getEditedPostContent ) + .toBe( ` +

Paragraph to be made "large"

+` ); + } ); + + test( 'should reset a named font size using the tools panel menu', async ( { + editor, + page, + } ) => { + await editor.openDocumentSettingsSidebar(); + await page.click( 'role=button[name="Add default block"i]' ); + await page.keyboard.type( + 'Paragraph with font size reset using tools panel menu' + ); + await page.click( + 'role=radiogroup[name="Font size"i] >> role=radio[name="Small"i]' + ); + + await expect.poll( editor.getEditedPostContent ) + .toBe( ` +

Paragraph with font size reset using tools panel menu

+` ); + + await page.click( 'role=button[name="Typography options"i]' ); + await page.click( 'role=menuitem[name="Reset Font size"i]' ); + + await expect.poll( editor.getEditedPostContent ) + .toBe( ` +

Paragraph with font size reset using tools panel menu

+` ); + } ); + + test( 'should reset a named font size using input field', async ( { + editor, + page, + pageUtils, + } ) => { + await editor.openDocumentSettingsSidebar(); + await page.click( 'role=button[name="Add default block"i]' ); + await page.keyboard.type( + 'Paragraph with font size reset using input field' + ); + await page.click( + 'role=radiogroup[name="Font size"i] >> role=radio[name="Small"i]' + ); + + await expect.poll( editor.getEditedPostContent ) + .toBe( ` +

Paragraph with font size reset using input field

+` ); + + await page.click( + 'role=region[name="Editor settings"i] >> role=button[name="Set custom size"i]' + ); + await page.click( 'role=spinbutton[name="Custom"i]' ); + await pageUtils.pressKeyWithModifier( 'primary', 'A' ); + await page.keyboard.press( 'Backspace' ); + + await expect.poll( editor.getEditedPostContent ) + .toBe( ` +

Paragraph with font size reset using input field

+` ); + } ); + } ); +} ); From 9ea80f6f363f847b31c5faa3bac6be1c05f5ef63 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Fri, 16 Dec 2022 12:52:08 +0100 Subject: [PATCH 38/51] Remove the nux package (#46110) --- .github/CODEOWNERS | 1 - docs/contributors/code/scripts.md | 1 - docs/manifest.json | 12 -- docs/reference-guides/README.md | 1 - docs/reference-guides/data/README.md | 1 - docs/reference-guides/data/data-core-nux.md | 99 ------------ docs/toc.json | 1 - lib/client-assets.php | 13 +- package-lock.json | 18 +-- package.json | 1 - packages/base-styles/_z-index.scss | 5 +- packages/nux/.npmrc | 1 - packages/nux/CHANGELOG.md | 124 --------------- packages/nux/README.md | 114 -------------- packages/nux/package.json | 50 ------ packages/nux/src/components/dot-tip/README.md | 38 ----- packages/nux/src/components/dot-tip/index.js | 93 ----------- .../nux/src/components/dot-tip/style.scss | 123 --------------- .../dot-tip/test/__snapshots__/index.js.snap | 46 ------ .../nux/src/components/dot-tip/test/index.js | 78 ---------- packages/nux/src/index.js | 13 -- packages/nux/src/store/actions.js | 52 ------- packages/nux/src/store/index.js | 36 ----- packages/nux/src/store/reducer.js | 70 --------- packages/nux/src/store/selectors.js | 81 ---------- packages/nux/src/store/test/actions.js | 40 ----- packages/nux/src/store/test/reducer.js | 69 --------- packages/nux/src/store/test/selectors.js | 146 ------------------ packages/nux/src/style.scss | 1 - .../local-storage-overrides.json | 6 - 30 files changed, 5 insertions(+), 1329 deletions(-) delete mode 100644 docs/reference-guides/data/data-core-nux.md delete mode 100644 packages/nux/.npmrc delete mode 100644 packages/nux/CHANGELOG.md delete mode 100644 packages/nux/README.md delete mode 100644 packages/nux/package.json delete mode 100644 packages/nux/src/components/dot-tip/README.md delete mode 100644 packages/nux/src/components/dot-tip/index.js delete mode 100644 packages/nux/src/components/dot-tip/style.scss delete mode 100644 packages/nux/src/components/dot-tip/test/__snapshots__/index.js.snap delete mode 100644 packages/nux/src/components/dot-tip/test/index.js delete mode 100644 packages/nux/src/index.js delete mode 100644 packages/nux/src/store/actions.js delete mode 100644 packages/nux/src/store/index.js delete mode 100644 packages/nux/src/store/reducer.js delete mode 100644 packages/nux/src/store/selectors.js delete mode 100644 packages/nux/src/store/test/actions.js delete mode 100644 packages/nux/src/store/test/reducer.js delete mode 100644 packages/nux/src/store/test/selectors.js delete mode 100644 packages/nux/src/style.scss diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 2cc7b0ce34bd1e..6427516035a5ec 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -87,7 +87,6 @@ /packages/compose @ajitbohra /packages/element @ajitbohra /packages/notices @ajitbohra -/packages/nux @ajitbohra /packages/viewport @ajitbohra /packages/base-styles /packages/icons diff --git a/docs/contributors/code/scripts.md b/docs/contributors/code/scripts.md index 3b36c8f4be4956..b7cabe0130d71c 100644 --- a/docs/contributors/code/scripts.md +++ b/docs/contributors/code/scripts.md @@ -31,7 +31,6 @@ The editor includes a number of packages to enable various pieces of functionali | [Is Shallow Equal](/packages/is-shallow-equal/README.md) | wp-is-shallow-equal | A function for performing a shallow comparison between two objects or arrays | | [Keycodes](/packages/keycodes/README.md) | wp-keycodes | Keycodes utilities for WordPress, used to check the key pressed in events like `onKeyDown` | | [List Reusable blocks](/packages/list-reusable-blocks/README.md) | wp-list-reusable-blocks | Package used to add import/export links to the listing page of the reusable blocks | -| [NUX](/packages/nux/README.md) | wp-nux | Components, and wp.data methods useful for onboarding a new user to the WordPress admin interface | | [Plugins](/packages/plugins/README.md) | wp-plugins | Plugins module for WordPress | | [Redux Routine](/packages/redux-routine/README.md) | wp-redux-routine | Redux middleware for generator coroutines | | [Rich Text](/packages/rich-text/README.md) | wp-rich-text | Helper functions to convert HTML or a DOM tree into a rich text value and back | diff --git a/docs/manifest.json b/docs/manifest.json index 8cc39af57531e4..a06df18c6c44b9 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -1733,12 +1733,6 @@ "markdown_source": "../packages/npm-package-json-lint-config/README.md", "parent": "packages" }, - { - "title": "@wordpress/nux", - "slug": "packages-nux", - "markdown_source": "../packages/nux/README.md", - "parent": "packages" - }, { "title": "@wordpress/plugins", "slug": "packages-plugins", @@ -1973,12 +1967,6 @@ "markdown_source": "../docs/reference-guides/data/data-core-notices.md", "parent": "data" }, - { - "title": "The NUX (New User Experience) Data", - "slug": "data-core-nux", - "markdown_source": "../docs/reference-guides/data/data-core-nux.md", - "parent": "data" - }, { "title": "Preferences", "slug": "data-core-preferences", diff --git a/docs/reference-guides/README.md b/docs/reference-guides/README.md index 33fdd9aa602414..f13c838697f2de 100644 --- a/docs/reference-guides/README.md +++ b/docs/reference-guides/README.md @@ -63,7 +63,6 @@ - [**core/editor**: The Post Editor’s Data](/docs/reference-guides/data/data-core-editor.md) - [**core/keyboard-shortcuts**: The Keyboard Shortcuts Data](/docs/reference-guides/data/data-core-keyboard-shortcuts.md) - [**core/notices**: Notices Data](/docs/reference-guides/data/data-core-notices.md) - - [**core/nux**: The NUX (New User Experience) Data](/docs/reference-guides/data/data-core-nux.md) - [**core/preferences**: Preferences](/docs/reference-guides/data/data-core-preferences.md) - [**core/reusable-blocks**: Reusable blocks](/docs/reference-guides/data/data-core-reusable-blocks.md) - [**core/rich-text**: Rich Text](/docs/reference-guides/data/data-core-rich-text.md) diff --git a/docs/reference-guides/data/README.md b/docs/reference-guides/data/README.md index 1134c1d5ddd307..5f4d8d92d4bd49 100644 --- a/docs/reference-guides/data/README.md +++ b/docs/reference-guides/data/README.md @@ -12,7 +12,6 @@ - [**core/editor**: The Post Editor’s Data](/docs/reference-guides/data/data-core-editor.md) - [**core/keyboard-shortcuts**: The Keyboard Shortcuts Data](/docs/reference-guides/data/data-core-keyboard-shortcuts.md) - [**core/notices**: Notices Data](/docs/reference-guides/data/data-core-notices.md) -- [**core/nux**: The NUX (New User Experience) Data](/docs/reference-guides/data/data-core-nux.md) - [**core/preferences**: Preferences](/docs/reference-guides/data/data-core-preferences.md) - [**core/reusable-blocks**: Reusable blocks](/docs/reference-guides/data/data-core-reusable-blocks.md) - [**core/rich-text**: Rich Text](/docs/reference-guides/data/data-core-rich-text.md) diff --git a/docs/reference-guides/data/data-core-nux.md b/docs/reference-guides/data/data-core-nux.md deleted file mode 100644 index 4d2e8a0d98d546..00000000000000 --- a/docs/reference-guides/data/data-core-nux.md +++ /dev/null @@ -1,99 +0,0 @@ -# The NUX (New User Experience) Data - -Namespace: `core/nux`. - -## Selectors - - - -### areTipsEnabled - -Returns whether or not tips are globally enabled. - -_Parameters_ - -- _state_ `Object`: Global application state. - -_Returns_ - -- `boolean`: Whether tips are globally enabled. - -### getAssociatedGuide - -Returns an object describing the guide, if any, that the given tip is a part -of. - -_Parameters_ - -- _state_ `Object`: Global application state. -- _tipId_ `string`: The tip to query. - -_Returns_ - -- `?NUXGuideInfo`: Information about the associated guide. - -### isTipVisible - -Determines whether or not the given tip is showing. Tips are hidden if they -are disabled, have been dismissed, or are not the current tip in any -guide that they have been added to. - -_Parameters_ - -- _state_ `Object`: Global application state. -- _tipId_ `string`: The tip to query. - -_Returns_ - -- `boolean`: Whether or not the given tip is showing. - - - -## Actions - - - -### disableTips - -Returns an action object that, when dispatched, prevents all tips from -showing again. - -_Returns_ - -- `Object`: Action object. - -### dismissTip - -Returns an action object that, when dispatched, dismisses the given tip. A -dismissed tip will not show again. - -_Parameters_ - -- _id_ `string`: The tip to dismiss. - -_Returns_ - -- `Object`: Action object. - -### enableTips - -Returns an action object that, when dispatched, makes all tips show again. - -_Returns_ - -- `Object`: Action object. - -### triggerGuide - -Returns an action object that, when dispatched, presents a guide that takes -the user through a series of tips step by step. - -_Parameters_ - -- _tipIds_ `string[]`: Which tips to show in the guide. - -_Returns_ - -- `Object`: Action object. - - diff --git a/docs/toc.json b/docs/toc.json index 532e6ef2d20e1d..4203f40c16cbc4 100644 --- a/docs/toc.json +++ b/docs/toc.json @@ -278,7 +278,6 @@ "docs/reference-guides/data/data-core-keyboard-shortcuts.md": [] }, { "docs/reference-guides/data/data-core-notices.md": [] }, - { "docs/reference-guides/data/data-core-nux.md": [] }, { "docs/reference-guides/data/data-core-preferences.md": [] }, diff --git a/lib/client-assets.php b/lib/client-assets.php index b7a11724a5e096..b7537057b89ab7 100644 --- a/lib/client-assets.php +++ b/lib/client-assets.php @@ -274,7 +274,7 @@ function gutenberg_register_packages_styles( $styles ) { $styles, 'wp-editor', gutenberg_url( 'build/editor/style.css' ), - array( 'wp-components', 'wp-block-editor', 'wp-nux', 'wp-reusable-blocks' ), + array( 'wp-components', 'wp-block-editor', 'wp-reusable-blocks' ), $version ); $styles->add_data( 'wp-editor', 'rtl', 'replace' ); @@ -283,7 +283,7 @@ function gutenberg_register_packages_styles( $styles ) { $styles, 'wp-edit-post', gutenberg_url( 'build/edit-post/style.css' ), - array( 'wp-components', 'wp-block-editor', 'wp-editor', 'wp-edit-blocks', 'wp-block-library', 'wp-nux' ), + array( 'wp-components', 'wp-block-editor', 'wp-editor', 'wp-edit-blocks', 'wp-block-library' ), $version ); $styles->add_data( 'wp-edit-post', 'rtl', 'replace' ); @@ -367,15 +367,6 @@ function gutenberg_register_packages_styles( $styles ) { ); $styles->add_data( 'wp-edit-blocks', 'rtl', 'replace' ); - gutenberg_override_style( - $styles, - 'wp-nux', - gutenberg_url( 'build/nux/style.css' ), - array( 'wp-components' ), - $version - ); - $styles->add_data( 'wp-nux', 'rtl', 'replace' ); - gutenberg_override_style( $styles, 'wp-block-library-theme', diff --git a/package-lock.json b/package-lock.json index 9906e5f5bac5c5..4cdf49e9da3232 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18980,20 +18980,6 @@ "version": "file:packages/npm-package-json-lint-config", "dev": true }, - "@wordpress/nux": { - "version": "file:packages/nux", - "requires": { - "@babel/runtime": "^7.16.0", - "@wordpress/components": "file:packages/components", - "@wordpress/compose": "file:packages/compose", - "@wordpress/data": "file:packages/data", - "@wordpress/deprecated": "file:packages/deprecated", - "@wordpress/element": "file:packages/element", - "@wordpress/i18n": "file:packages/i18n", - "@wordpress/icons": "file:packages/icons", - "rememo": "^4.0.0" - } - }, "@wordpress/plugins": { "version": "file:packages/plugins", "requires": { @@ -32020,7 +32006,7 @@ "css.escape": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", - "integrity": "sha1-QuJ9T6BK4y+TGktNQZH6nN3ul8s=", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", "dev": true }, "cssesc": { @@ -44902,7 +44888,7 @@ "lz-string": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz", - "integrity": "sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY=", + "integrity": "sha512-0ckx7ZHRPqb0oUm8zNr+90mtf9DQB60H1wMCjBtfi62Kl3a7JbHob6gA2bC+xRvZoOL+1hzUK8jeuEIQE8svEQ==", "dev": true }, "macos-release": { diff --git a/package.json b/package.json index 93c17bc1d92cdd..8c87dcaae93a7d 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,6 @@ "@wordpress/list-reusable-blocks": "file:packages/list-reusable-blocks", "@wordpress/media-utils": "file:packages/media-utils", "@wordpress/notices": "file:packages/notices", - "@wordpress/nux": "file:packages/nux", "@wordpress/plugins": "file:packages/plugins", "@wordpress/preferences": "file:packages/preferences", "@wordpress/preferences-persistence": "file:packages/preferences-persistence", diff --git a/packages/base-styles/_z-index.scss b/packages/base-styles/_z-index.scss index 62a1ab3b19d8f0..5c9af46fec6e6f 100644 --- a/packages/base-styles/_z-index.scss +++ b/packages/base-styles/_z-index.scss @@ -145,10 +145,7 @@ $z-layers: ( // The focus styles of the region navigation containers should be above their content. ".is-focusing-regions {region} :focus::after": 1000000, - // Show NUX tips above popovers, wp-admin menus, submenus, and sidebar: - ".nux-dot-tip": 1000001, - - // Show tooltips above NUX tips, wp-admin menus, submenus, and sidebar: + // Show tooltips above wp-admin menus, submenus, and sidebar: ".components-tooltip": 1000002, // Keep template popover underneath 'Create custom template' modal overlay. diff --git a/packages/nux/.npmrc b/packages/nux/.npmrc deleted file mode 100644 index 43c97e719a5a82..00000000000000 --- a/packages/nux/.npmrc +++ /dev/null @@ -1 +0,0 @@ -package-lock=false diff --git a/packages/nux/CHANGELOG.md b/packages/nux/CHANGELOG.md deleted file mode 100644 index edbf4c88a21f0f..00000000000000 --- a/packages/nux/CHANGELOG.md +++ /dev/null @@ -1,124 +0,0 @@ - - -## Unreleased - -### Breaking Changes - -- Updated dependencies to require React 18 ([45235](https://github.com/WordPress/gutenberg/pull/45235)) - -## 5.20.0 (2022-11-16) - -## 5.19.0 (2022-11-02) - -## 5.18.0 (2022-10-19) - -## 5.17.0 (2022-10-05) - -## 5.16.0 (2022-09-21) - -## 5.15.0 (2022-09-13) - -## 5.14.0 (2022-08-24) - -## 5.13.0 (2022-08-10) - -## 5.12.0 (2022-07-27) - -## 5.11.0 (2022-07-13) - -## 5.10.0 (2022-06-29) - -## 5.9.0 (2022-06-15) - -## 5.8.0 (2022-06-01) - -## 5.7.0 (2022-05-18) - -## 5.6.0 (2022-05-04) - -## 5.5.0 (2022-04-21) - -## 5.4.0 (2022-04-08) - -## 5.3.0 (2022-03-23) - -## 5.2.0 (2022-03-11) - -## 5.1.0 (2022-01-27) - -## 5.0.0 (2021-07-29) - -### Breaking Change - -- Upgraded React components to work with v17.0 ([#29118](https://github.com/WordPress/gutenberg/pull/29118)). There are no new features in React v17.0 as explained in the [blog post](https://reactjs.org/blog/2020/10/20/react-v17.html). - -## 4.2.0 (2021-07-21) - -## 4.1.0 (2021-05-20) - -## 4.0.0 (2021-05-14) - -### Breaking Changes - -- Drop support for Internet Explorer 11 ([#31110](https://github.com/WordPress/gutenberg/pull/31110)). Learn more at https://make.wordpress.org/core/2021/04/22/ie-11-support-phase-out-plan/. -- Increase the minimum Node.js version to v12 matching Long Term Support releases ([#31270](https://github.com/WordPress/gutenberg/pull/31270)). Learn more at https://nodejs.org/en/about/releases/. - -## 3.25.0 (2021-03-17) - -## 3.24.0 (2020-12-17) - -### New Feature - -- Added a store definition `store` for the core data namespace to use with `@wordpress/data` API ([#26655](https://github.com/WordPress/gutenberg/pull/26655)). - -# 3.1.0 (2019-06-03) - -- The `@wordpress/nux` package has been deprecated. Please use the `Guide` component in `@wordpress/components` to show a user guide. - -## 3.0.6 (2019-01-03) - -## 3.0.5 (2018-12-12) - -## 3.0.4 (2018-11-30) - -## 3.0.3 (2018-11-22) - -## 3.0.2 (2018-11-21) - -## 3.0.1 (2018-11-20) - -## 3.0.0 (2018-11-15) - -### Breaking Changes - -- The id prop of DotTip has been removed. Please use the tipId prop instead. - -## 2.0.13 (2018-11-12) - -## 2.0.12 (2018-11-12) - -## 2.0.11 (2018-11-09) - -## 2.0.10 (2018-11-09) - -## 2.0.9 (2018-11-03) - -## 2.0.8 (2018-10-30) - -## 2.0.7 (2018-10-29) - -### Deprecations - -- The id prop of DotTip has been deprecated. Please use the tipId prop instead. - -## 2.0.6 (2018-10-22) - -## 2.0.5 (2018-10-19) - -## 2.0.4 (2018-10-18) - -## 2.0.0 (2018-09-05) - -### Breaking Change - -- Change how required built-ins are polyfilled with Babel 7 ([#9171](https://github.com/WordPress/gutenberg/pull/9171)). If you're using an environment that has limited or no support for ES2015+ such as lower versions of IE then using [core-js](https://github.com/zloirock/core-js) or [@babel/polyfill](https://babeljs.io/docs/en/next/babel-polyfill) will add support for these methods. diff --git a/packages/nux/README.md b/packages/nux/README.md deleted file mode 100644 index c0941ddd0c5f2a..00000000000000 --- a/packages/nux/README.md +++ /dev/null @@ -1,114 +0,0 @@ -# New User eXperience (NUX) - -The NUX module exposes components, and `wp.data` methods useful for onboarding a new user to the WordPress admin interface. Specifically, it exposes _tips_ and _guides_. - -A _tip_ is a component that points to an element in the UI and contains text that explains the element's functionality. The user can dismiss a tip, in which case it never shows again. The user can also disable tips entirely. Information about tips is persisted between sessions using `localStorage`. - -A _guide_ allows a series of tips to be presented to the user one by one. When a user dismisses a tip that is in a guide, the next tip in the guide is shown. - -## Installation - -Install the module - -```bash -npm install @wordpress/nux --save -``` - -_This package assumes that your code will run in an **ES2015+** environment. If you're using an environment that has limited or no support for such language features and APIs, you should include [the polyfill shipped in `@wordpress/babel-preset-default`](https://github.com/WordPress/gutenberg/tree/HEAD/packages/babel-preset-default#polyfill) in your code._ - -## DotTip - -`DotTip` is a React component that renders a single _tip_ on the screen. The tip will point to the React element that `DotTip` is nested within. Each tip is uniquely identified by a string passed to `tipId`. - -See [the component's README][dot-tip-readme] for more information. - -[dot-tip-readme]: https://github.com/WordPress/gutenberg/tree/HEAD/packages/nux/src/components/dot-tip/README.md - -```jsx - -} -``` - -## Determining if a tip is visible - -You can programmatically determine if a tip is visible using the `isTipVisible` select method. - -```jsx -const isVisible = select( 'core/nux' ).isTipVisible( 'acme/add-to-cart' ); -console.log( isVisible ); // true or false -``` - -## Manually dismissing a tip - -`dismissTip` is a dispatch method that allows you to programmatically dismiss a tip. - -```jsx - -``` - -## Disabling and enabling tips - -Tips can be programatically disabled or enabled using the `disableTips` and `enableTips` dispatch methods. You can query the current setting by using the `areTipsEnabled` select method. - -Calling `enableTips` will also un-dismiss all previously dismissed tips. - -```jsx -const areTipsEnabled = select( 'core/nux' ).areTipsEnabled(); -return ( - -); -``` - -## Triggering a guide - -You can group a series of tips into a guide by calling the `triggerGuide` dispatch method. The given tips will then appear one by one. - -A tip cannot be added to more than one guide. - -```jsx -dispatch( 'core/nux' ).triggerGuide( [ - 'acme/product-info', - 'acme/add-to-cart', - 'acme/checkout', -] ); -``` - -## Getting information about a guide - -`getAssociatedGuide` is a select method that returns useful information about the state of the guide that a tip is associated with. - -```jsx -const guide = select( 'core/nux' ).getAssociatedGuide( 'acme/add-to-cart' ); -console.log( 'Tips in this guide:', guide.tipIds ); -console.log( 'Currently showing:', guide.currentTipId ); -console.log( 'Next to show:', guide.nextTipId ); -``` - -## Contributing to this package - -This is an individual package that's part of the Gutenberg project. The project is organized as a monorepo. It's made up of multiple self-contained software packages, each with a specific purpose. The packages in this monorepo are published to [npm](https://www.npmjs.com/) and used by [WordPress](https://make.wordpress.org/core/) as well as other software projects. - -To find out more about contributing to this package or Gutenberg as a whole, please read the project's main [contributor guide](https://github.com/WordPress/gutenberg/tree/HEAD/CONTRIBUTING.md). - -

Code is Poetry.

diff --git a/packages/nux/package.json b/packages/nux/package.json deleted file mode 100644 index 9089f865e729de..00000000000000 --- a/packages/nux/package.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "name": "@wordpress/nux", - "version": "5.20.0", - "description": "NUX (New User eXperience) module for WordPress.", - "author": "The WordPress Contributors", - "license": "GPL-2.0-or-later", - "keywords": [ - "wordpress", - "gutenberg", - "nux" - ], - "homepage": "https://github.com/WordPress/gutenberg/tree/HEAD/packages/nux/README.md", - "repository": { - "type": "git", - "url": "https://github.com/WordPress/gutenberg.git", - "directory": "packages/nux" - }, - "bugs": { - "url": "https://github.com/WordPress/gutenberg/issues" - }, - "engines": { - "node": ">=12" - }, - "main": "build/index.js", - "module": "build-module/index.js", - "react-native": "src/index", - "sideEffects": [ - "build-style/**", - "src/**/*.scss", - "{src,build,build-module}/{index.js,store/index.js}" - ], - "dependencies": { - "@babel/runtime": "^7.16.0", - "@wordpress/components": "file:../components", - "@wordpress/compose": "file:../compose", - "@wordpress/data": "file:../data", - "@wordpress/deprecated": "file:../deprecated", - "@wordpress/element": "file:../element", - "@wordpress/i18n": "file:../i18n", - "@wordpress/icons": "file:../icons", - "rememo": "^4.0.0" - }, - "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" - }, - "publishConfig": { - "access": "public" - } -} diff --git a/packages/nux/src/components/dot-tip/README.md b/packages/nux/src/components/dot-tip/README.md deleted file mode 100644 index f143a22a222588..00000000000000 --- a/packages/nux/src/components/dot-tip/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# DotTip - -`DotTip` is a React component that renders a single _tip_ on the screen. The tip will point to the React element that `DotTip` is nested within. Each tip is uniquely identified by a string passed to `tipId`. - -## Usage - -```jsx - -} -``` - -## Props - -The component accepts the following props: - -### tipId - -A string that uniquely identifies the tip. Identifiers should be prefixed with the name of the plugin, followed by a `/`. For example, `acme/add-to-cart`. - -- Type: `string` -- Required: Yes - -### position - -The direction in which the popover should open relative to its parent node. Specify y- and x-axis as a space-separated string. Supports `"top"`, `"middle"`, `"bottom"` y axis, and `"left"`, `"center"`, `"right"` x axis. - -- Type: `String` -- Required: No -- Default: `"middle right"` - -### children - -Any React element or elements can be passed as children. They will be rendered within the tip bubble. diff --git a/packages/nux/src/components/dot-tip/index.js b/packages/nux/src/components/dot-tip/index.js deleted file mode 100644 index 50de7ddb3be9df..00000000000000 --- a/packages/nux/src/components/dot-tip/index.js +++ /dev/null @@ -1,93 +0,0 @@ -/** - * WordPress dependencies - */ -import { compose } from '@wordpress/compose'; -import { Popover, Button } from '@wordpress/components'; -import { __ } from '@wordpress/i18n'; -import { withSelect, withDispatch } from '@wordpress/data'; -import { useCallback, useRef } from '@wordpress/element'; -import { close } from '@wordpress/icons'; - -/** - * Internal dependencies - */ -import { store as nuxStore } from '../../store'; - -function onClick( event ) { - // Tips are often nested within buttons. We stop propagation so that clicking - // on a tip doesn't result in the button being clicked. - event.stopPropagation(); -} - -export function DotTip( { - position = 'middle right', - children, - isVisible, - hasNextTip, - onDismiss, - onDisable, -} ) { - const anchorParent = useRef( null ); - const onFocusOutsideCallback = useCallback( - ( event ) => { - if ( ! anchorParent.current ) { - return; - } - if ( anchorParent.current.contains( event.relatedTarget ) ) { - return; - } - onDisable(); - }, - [ onDisable, anchorParent ] - ); - if ( ! isVisible ) { - return null; - } - - return ( - -

{ children }

-

- -

- -

- -
-
-`; diff --git a/packages/nux/src/components/dot-tip/test/index.js b/packages/nux/src/components/dot-tip/test/index.js deleted file mode 100644 index 7917b044fc3e2d..00000000000000 --- a/packages/nux/src/components/dot-tip/test/index.js +++ /dev/null @@ -1,78 +0,0 @@ -/** - * External dependencies - */ -import { act, render, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; - -/** - * Internal dependencies - */ -import { DotTip } from '..'; - -const noop = () => {}; - -describe( 'DotTip', () => { - it( 'should not render anything if invisible', async () => { - render( - - It looks like you’re writing a letter. Would you like help? - - ); - - await act( () => Promise.resolve() ); - - expect( screen.queryByRole( 'dialog' ) ).not.toBeInTheDocument(); - } ); - - it( 'should render correctly', async () => { - render( - - It looks like you’re writing a letter. Would you like help? - - ); - - await act( () => Promise.resolve() ); - - expect( screen.getByRole( 'dialog' ) ).toMatchSnapshot(); - } ); - - it( 'should call onDismiss when the dismiss button is clicked', async () => { - const user = userEvent.setup( { - advanceTimers: jest.advanceTimersByTime, - } ); - const onDismiss = jest.fn(); - - render( - - It looks like you’re writing a letter. Would you like help? - - ); - - await act( () => Promise.resolve() ); - - await user.click( screen.getByRole( 'button', { name: 'Got it' } ) ); - - expect( onDismiss ).toHaveBeenCalled(); - } ); - - it( 'should call onDisable when the X button is clicked', async () => { - const user = userEvent.setup( { - advanceTimers: jest.advanceTimersByTime, - } ); - const onDisable = jest.fn(); - - render( - - It looks like you’re writing a letter. Would you like help? - - ); - - await act( () => Promise.resolve() ); - - await user.click( - screen.getByRole( 'button', { name: 'Disable tips' } ) - ); - - expect( onDisable ).toHaveBeenCalled(); - } ); -} ); diff --git a/packages/nux/src/index.js b/packages/nux/src/index.js deleted file mode 100644 index a0b3e073503750..00000000000000 --- a/packages/nux/src/index.js +++ /dev/null @@ -1,13 +0,0 @@ -/** - * WordPress dependencies - */ -import deprecated from '@wordpress/deprecated'; - -export { store } from './store'; -export { default as DotTip } from './components/dot-tip'; - -deprecated( 'wp.nux', { - since: '5.4', - hint: 'wp.components.Guide can be used to show a user guide.', - version: '6.2', -} ); diff --git a/packages/nux/src/store/actions.js b/packages/nux/src/store/actions.js deleted file mode 100644 index ad8adb79c5530d..00000000000000 --- a/packages/nux/src/store/actions.js +++ /dev/null @@ -1,52 +0,0 @@ -/** - * Returns an action object that, when dispatched, presents a guide that takes - * the user through a series of tips step by step. - * - * @param {string[]} tipIds Which tips to show in the guide. - * - * @return {Object} Action object. - */ -export function triggerGuide( tipIds ) { - return { - type: 'TRIGGER_GUIDE', - tipIds, - }; -} - -/** - * Returns an action object that, when dispatched, dismisses the given tip. A - * dismissed tip will not show again. - * - * @param {string} id The tip to dismiss. - * - * @return {Object} Action object. - */ -export function dismissTip( id ) { - return { - type: 'DISMISS_TIP', - id, - }; -} - -/** - * Returns an action object that, when dispatched, prevents all tips from - * showing again. - * - * @return {Object} Action object. - */ -export function disableTips() { - return { - type: 'DISABLE_TIPS', - }; -} - -/** - * Returns an action object that, when dispatched, makes all tips show again. - * - * @return {Object} Action object. - */ -export function enableTips() { - return { - type: 'ENABLE_TIPS', - }; -} diff --git a/packages/nux/src/store/index.js b/packages/nux/src/store/index.js deleted file mode 100644 index 39fef6c78c7911..00000000000000 --- a/packages/nux/src/store/index.js +++ /dev/null @@ -1,36 +0,0 @@ -/** - * WordPress dependencies - */ -import { createReduxStore, registerStore } from '@wordpress/data'; - -/** - * Internal dependencies - */ -import reducer from './reducer'; -import * as actions from './actions'; -import * as selectors from './selectors'; - -const STORE_NAME = 'core/nux'; - -/** - * Store definition for the nux namespace. - * - * @see https://github.com/WordPress/gutenberg/blob/HEAD/packages/data/README.md#createReduxStore - * - * @type {Object} - */ -export const store = createReduxStore( STORE_NAME, { - reducer, - actions, - selectors, - persist: [ 'preferences' ], -} ); - -// Once we build a more generic persistence plugin that works across types of stores -// we'd be able to replace this with a register call. -registerStore( STORE_NAME, { - reducer, - actions, - selectors, - persist: [ 'preferences' ], -} ); diff --git a/packages/nux/src/store/reducer.js b/packages/nux/src/store/reducer.js deleted file mode 100644 index 373e4781f52353..00000000000000 --- a/packages/nux/src/store/reducer.js +++ /dev/null @@ -1,70 +0,0 @@ -/** - * WordPress dependencies - */ -import { combineReducers } from '@wordpress/data'; - -/** - * Reducer that tracks which tips are in a guide. Each guide is represented by - * an array which contains the tip identifiers contained within that guide. - * - * @param {Array} state Current state. - * @param {Object} action Dispatched action. - * - * @return {Array} Updated state. - */ -export function guides( state = [], action ) { - switch ( action.type ) { - case 'TRIGGER_GUIDE': - return [ ...state, action.tipIds ]; - } - - return state; -} - -/** - * Reducer that tracks whether or not tips are globally enabled. - * - * @param {boolean} state Current state. - * @param {Object} action Dispatched action. - * - * @return {boolean} Updated state. - */ -export function areTipsEnabled( state = true, action ) { - switch ( action.type ) { - case 'DISABLE_TIPS': - return false; - - case 'ENABLE_TIPS': - return true; - } - - return state; -} - -/** - * Reducer that tracks which tips have been dismissed. If the state object - * contains a tip identifier, then that tip is dismissed. - * - * @param {Object} state Current state. - * @param {Object} action Dispatched action. - * - * @return {Object} Updated state. - */ -export function dismissedTips( state = {}, action ) { - switch ( action.type ) { - case 'DISMISS_TIP': - return { - ...state, - [ action.id ]: true, - }; - - case 'ENABLE_TIPS': - return {}; - } - - return state; -} - -const preferences = combineReducers( { areTipsEnabled, dismissedTips } ); - -export default combineReducers( { guides, preferences } ); diff --git a/packages/nux/src/store/selectors.js b/packages/nux/src/store/selectors.js deleted file mode 100644 index e87cf688a1ba32..00000000000000 --- a/packages/nux/src/store/selectors.js +++ /dev/null @@ -1,81 +0,0 @@ -/** - * External dependencies - */ -import createSelector from 'rememo'; - -/** - * An object containing information about a guide. - * - * @typedef {Object} NUXGuideInfo - * @property {string[]} tipIds Which tips the guide contains. - * @property {?string} currentTipId The guide's currently showing tip. - * @property {?string} nextTipId The guide's next tip to show. - */ - -/** - * Returns an object describing the guide, if any, that the given tip is a part - * of. - * - * @param {Object} state Global application state. - * @param {string} tipId The tip to query. - * - * @return {?NUXGuideInfo} Information about the associated guide. - */ -export const getAssociatedGuide = createSelector( - ( state, tipId ) => { - for ( const tipIds of state.guides ) { - if ( tipIds.includes( tipId ) ) { - const nonDismissedTips = tipIds.filter( - ( tId ) => - ! Object.keys( - state.preferences.dismissedTips - ).includes( tId ) - ); - const [ currentTipId = null, nextTipId = null ] = - nonDismissedTips; - return { tipIds, currentTipId, nextTipId }; - } - } - - return null; - }, - ( state ) => [ state.guides, state.preferences.dismissedTips ] -); - -/** - * Determines whether or not the given tip is showing. Tips are hidden if they - * are disabled, have been dismissed, or are not the current tip in any - * guide that they have been added to. - * - * @param {Object} state Global application state. - * @param {string} tipId The tip to query. - * - * @return {boolean} Whether or not the given tip is showing. - */ -export function isTipVisible( state, tipId ) { - if ( ! state.preferences.areTipsEnabled ) { - return false; - } - - if ( state.preferences.dismissedTips?.hasOwnProperty( tipId ) ) { - return false; - } - - const associatedGuide = getAssociatedGuide( state, tipId ); - if ( associatedGuide && associatedGuide.currentTipId !== tipId ) { - return false; - } - - return true; -} - -/** - * Returns whether or not tips are globally enabled. - * - * @param {Object} state Global application state. - * - * @return {boolean} Whether tips are globally enabled. - */ -export function areTipsEnabled( state ) { - return state.preferences.areTipsEnabled; -} diff --git a/packages/nux/src/store/test/actions.js b/packages/nux/src/store/test/actions.js deleted file mode 100644 index 4e22afe03c8b82..00000000000000 --- a/packages/nux/src/store/test/actions.js +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Internal dependencies - */ -import { triggerGuide, dismissTip, disableTips, enableTips } from '../actions'; - -describe( 'actions', () => { - describe( 'triggerGuide', () => { - it( 'should return a TRIGGER_GUIDE action', () => { - expect( triggerGuide( [ 'test/tip-1', 'test/tip-2' ] ) ).toEqual( { - type: 'TRIGGER_GUIDE', - tipIds: [ 'test/tip-1', 'test/tip-2' ], - } ); - } ); - } ); - - describe( 'dismissTip', () => { - it( 'should return an DISMISS_TIP action', () => { - expect( dismissTip( 'test/tip' ) ).toEqual( { - type: 'DISMISS_TIP', - id: 'test/tip', - } ); - } ); - } ); - - describe( 'disableTips', () => { - it( 'should return an DISABLE_TIPS action', () => { - expect( disableTips() ).toEqual( { - type: 'DISABLE_TIPS', - } ); - } ); - } ); - - describe( 'enableTips', () => { - it( 'should return an ENABLE_TIPS action', () => { - expect( enableTips() ).toEqual( { - type: 'ENABLE_TIPS', - } ); - } ); - } ); -} ); diff --git a/packages/nux/src/store/test/reducer.js b/packages/nux/src/store/test/reducer.js deleted file mode 100644 index 49172442d8f379..00000000000000 --- a/packages/nux/src/store/test/reducer.js +++ /dev/null @@ -1,69 +0,0 @@ -/** - * Internal dependencies - */ -import { guides, areTipsEnabled, dismissedTips } from '../reducer'; - -describe( 'reducer', () => { - describe( 'guides', () => { - it( 'should start out empty', () => { - expect( guides( undefined, {} ) ).toEqual( [] ); - } ); - - it( 'should add a guide when it is triggered', () => { - const state = guides( [], { - type: 'TRIGGER_GUIDE', - tipIds: [ 'test/tip-1', 'test/tip-2' ], - } ); - expect( state ).toEqual( [ [ 'test/tip-1', 'test/tip-2' ] ] ); - } ); - } ); - - describe( 'areTipsEnabled', () => { - it( 'should default to true', () => { - expect( areTipsEnabled( undefined, {} ) ).toBe( true ); - } ); - - it( 'should flip when tips are disabled', () => { - const state = areTipsEnabled( true, { - type: 'DISABLE_TIPS', - } ); - expect( state ).toBe( false ); - } ); - - it( 'should flip when tips are enabled', () => { - const state = areTipsEnabled( false, { - type: 'ENABLE_TIPS', - } ); - expect( state ).toBe( true ); - } ); - } ); - - describe( 'dismissedTips', () => { - it( 'should start out empty', () => { - expect( dismissedTips( undefined, {} ) ).toEqual( {} ); - } ); - - it( 'should mark tips as dismissed', () => { - const state = dismissedTips( - {}, - { - type: 'DISMISS_TIP', - id: 'test/tip', - } - ); - expect( state ).toEqual( { - 'test/tip': true, - } ); - } ); - - it( 'should reset if tips are enabled', () => { - const initialState = { - 'test/tip': true, - }; - const state = dismissedTips( initialState, { - type: 'ENABLE_TIPS', - } ); - expect( state ).toEqual( {} ); - } ); - } ); -} ); diff --git a/packages/nux/src/store/test/selectors.js b/packages/nux/src/store/test/selectors.js deleted file mode 100644 index e2a06c74e08b68..00000000000000 --- a/packages/nux/src/store/test/selectors.js +++ /dev/null @@ -1,146 +0,0 @@ -/** - * Internal dependencies - */ -import { getAssociatedGuide, isTipVisible, areTipsEnabled } from '../selectors'; - -describe( 'selectors', () => { - describe( 'getAssociatedGuide', () => { - const state = { - guides: [ - [ 'test/tip-1', 'test/tip-2', 'test/tip-3' ], - [ 'test/tip-a', 'test/tip-b', 'test/tip-c' ], - [ 'test/tip-α', 'test/tip-β', 'test/tip-γ' ], - ], - preferences: { - dismissedTips: { - 'test/tip-1': true, - 'test/tip-a': true, - 'test/tip-b': true, - 'test/tip-α': true, - 'test/tip-β': true, - 'test/tip-γ': true, - }, - }, - }; - - it( 'should return null when there is no associated guide', () => { - expect( getAssociatedGuide( state, 'test/unknown' ) ).toBeNull(); - } ); - - it( 'should return the associated guide', () => { - expect( getAssociatedGuide( state, 'test/tip-2' ) ).toEqual( { - tipIds: [ 'test/tip-1', 'test/tip-2', 'test/tip-3' ], - currentTipId: 'test/tip-2', - nextTipId: 'test/tip-3', - } ); - } ); - - it( 'should indicate when there is no next tip', () => { - expect( getAssociatedGuide( state, 'test/tip-b' ) ).toEqual( { - tipIds: [ 'test/tip-a', 'test/tip-b', 'test/tip-c' ], - currentTipId: 'test/tip-c', - nextTipId: null, - } ); - } ); - - it( 'should indicate when there is no current or next tip', () => { - expect( getAssociatedGuide( state, 'test/tip-β' ) ).toEqual( { - tipIds: [ 'test/tip-α', 'test/tip-β', 'test/tip-γ' ], - currentTipId: null, - nextTipId: null, - } ); - } ); - } ); - - describe( 'isTipVisible', () => { - it( 'is tolerant to individual preferences being undefined', () => { - // See: https://github.com/WordPress/gutenberg/issues/14580 - const state = { - guides: [], - preferences: {}, - }; - expect( isTipVisible( state, 'test/tip' ) ).toBe( false ); - } ); - - it( 'is tolerant to undefined dismissedTips', () => { - // See: https://github.com/WordPress/gutenberg/issues/14580 - const state = { - guides: [], - preferences: { - areTipsEnabled: true, - }, - }; - expect( isTipVisible( state, 'test/tip' ) ).toBe( true ); - } ); - - it( 'should return true by default', () => { - const state = { - guides: [], - preferences: { - areTipsEnabled: true, - dismissedTips: {}, - }, - }; - expect( isTipVisible( state, 'test/tip' ) ).toBe( true ); - } ); - - it( 'should return false if tips are disabled', () => { - const state = { - guides: [], - preferences: { - areTipsEnabled: false, - dismissedTips: {}, - }, - }; - expect( isTipVisible( state, 'test/tip' ) ).toBe( false ); - } ); - - it( 'should return false if the tip is dismissed', () => { - const state = { - guides: [], - preferences: { - areTipsEnabled: true, - dismissedTips: { - 'test/tip': true, - }, - }, - }; - expect( isTipVisible( state, 'test/tip' ) ).toBe( false ); - } ); - - it( 'should return false if the tip is in a guide and it is not the current tip', () => { - const state = { - guides: [ [ 'test/tip-1', 'test/tip-2', 'test/tip-3' ] ], - preferences: { - areTipsEnabled: true, - dismissedTips: {}, - }, - }; - expect( isTipVisible( state, 'test/tip-2' ) ).toBe( false ); - } ); - } ); - - describe( 'areTipsEnabled', () => { - it( 'should return true if tips are enabled', () => { - const state = { - guides: [], - preferences: { - areTipsEnabled: true, - dismissedTips: {}, - }, - }; - expect( areTipsEnabled( state ) ).toBe( true ); - } ); - - it( 'should return false if tips are disabled', () => { - const state = { - guides: [], - preferences: { - areTipsEnabled: false, - dismissedTips: {}, - }, - }; - expect( areTipsEnabled( state ) ).toBe( false ); - } ); - } ); -} ); diff --git a/packages/nux/src/style.scss b/packages/nux/src/style.scss deleted file mode 100644 index 0df73ff851e9f9..00000000000000 --- a/packages/nux/src/style.scss +++ /dev/null @@ -1 +0,0 @@ -@import "./components/dot-tip/style.scss"; diff --git a/packages/react-native-bridge/common/gutenberg-web-single-block/local-storage-overrides.json b/packages/react-native-bridge/common/gutenberg-web-single-block/local-storage-overrides.json index f9cee4142d11ac..28c0c1b17b4adc 100644 --- a/packages/react-native-bridge/common/gutenberg-web-single-block/local-storage-overrides.json +++ b/packages/react-native-bridge/common/gutenberg-web-single-block/local-storage-overrides.json @@ -13,11 +13,5 @@ "hiddenBlockTypes": [], "preferredStyleVariations": {} } - }, - "core/nux": { - "preferences": { - "areTipsEnabled": false, - "dismissedTips": {} - } } } From cd8451d98b98dbd1150e6d0aa96bd93180d57ea8 Mon Sep 17 00:00:00 2001 From: Jarda Snajdr Date: Fri, 16 Dec 2022 13:11:59 +0100 Subject: [PATCH 39/51] useSelect: add unit tests for static select mode (#46606) --- .../src/components/use-select/test/index.js | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/packages/data/src/components/use-select/test/index.js b/packages/data/src/components/use-select/test/index.js index 6dccfd74b741fc..4b7babcedeaac7 100644 --- a/packages/data/src/components/use-select/test/index.js +++ b/packages/data/src/components/use-select/test/index.js @@ -1146,4 +1146,38 @@ describe( 'useSelect', () => { expect( screen.getByRole( 'status' ) ).toHaveTextContent( '10' ); } ); } ); + + describe( 'static store selection mode', () => { + it( 'can read the current value from store', () => { + registry.registerStore( 'testStore', { + reducer: ( s = 0, a ) => ( a.type === 'INC' ? s + 1 : s ), + actions: { inc: () => ( { type: 'INC' } ) }, + selectors: { get: ( s ) => s }, + } ); + + const record = jest.fn(); + + function TestComponent() { + const { get } = useSelect( 'testStore' ); + return ( + + ); + } + + render( + + + + ); + + fireEvent.click( screen.getByRole( 'button' ) ); + expect( record ).toHaveBeenLastCalledWith( 0 ); + + // no need to act() as the component doesn't react to the updates + registry.dispatch( 'testStore' ).inc(); + + fireEvent.click( screen.getByRole( 'button' ) ); + expect( record ).toHaveBeenLastCalledWith( 1 ); + } ); + } ); } ); From 7ab478bf01314913a660b5c1c3f9e016f14d46c9 Mon Sep 17 00:00:00 2001 From: Jarda Snajdr Date: Fri, 16 Dec 2022 13:16:36 +0100 Subject: [PATCH 40/51] Data: recreate listeningStores set for every markListeningStores call (#46607) --- packages/data/src/registry.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/data/src/registry.js b/packages/data/src/registry.js index 89286f689294a5..df46227257f363 100644 --- a/packages/data/src/registry.js +++ b/packages/data/src/registry.js @@ -60,7 +60,7 @@ function getStoreName( storeNameOrDescriptor ) { export function createRegistry( storeConfigs = {}, parent = null ) { const stores = {}; const emitter = createEmitter(); - const listeningStores = new Set(); + let listeningStores = null; /** * Global listener called for each store's update. @@ -112,7 +112,7 @@ export function createRegistry( storeConfigs = {}, parent = null ) { */ function select( storeNameOrDescriptor ) { const storeName = getStoreName( storeNameOrDescriptor ); - listeningStores.add( storeName ); + listeningStores?.add( storeName ); const store = stores[ storeName ]; if ( store ) { return store.getSelectors(); @@ -122,11 +122,12 @@ export function createRegistry( storeConfigs = {}, parent = null ) { } function __unstableMarkListeningStores( callback, ref ) { - listeningStores.clear(); + listeningStores = new Set(); try { return callback.call( this ); } finally { ref.current = Array.from( listeningStores ); + listeningStores = null; } } @@ -143,7 +144,7 @@ export function createRegistry( storeConfigs = {}, parent = null ) { */ function resolveSelect( storeNameOrDescriptor ) { const storeName = getStoreName( storeNameOrDescriptor ); - listeningStores.add( storeName ); + listeningStores?.add( storeName ); const store = stores[ storeName ]; if ( store ) { return store.getResolveSelectors(); @@ -165,7 +166,7 @@ export function createRegistry( storeConfigs = {}, parent = null ) { */ function suspendSelect( storeNameOrDescriptor ) { const storeName = getStoreName( storeNameOrDescriptor ); - listeningStores.add( storeName ); + listeningStores?.add( storeName ); const store = stores[ storeName ]; if ( store ) { return store.getSuspendSelectors(); From 89998fdfd4d8763cd7396e040db0a85f0c59b13e Mon Sep 17 00:00:00 2001 From: Andrei Draganescu Date: Fri, 16 Dec 2022 14:44:51 +0200 Subject: [PATCH 41/51] Adds page list as child of submenu (#46414) * adds page list as child of submenu and disables direct insert * fix page link CSS when adding to submenu, enable transform to pagelist, restore direct insert * try to not render a wrapper if page list block is nested * Fixes styling, aligns off canvas implementation to list view, fixes styling bug of page list Co-authored-by: Dave Smith <444434+getdave@users.noreply.github.com> Co-authored-by: Maggie <3593343+MaggieCabrera@users.noreply.github.com> Co-authored-by: Ben Dwyer <275961+scruffian@users.noreply.github.com> * fix fixture * add missing useEffect * Fixes markup rendering in the editor to avoid a double LI element Co-authored-by: Ben Dwyer <275961+scruffian@users.noreply.github.com> Co-authored-by: Maggie <3593343+MaggieCabrera@users.noreply.github.com> * added submenu container class to the page list item uls * Ensure submenus are correctly positioned in the editor * add a comment Co-authored-by: Andrei Draganescu Co-authored-by: Dave Smith <444434+getdave@users.noreply.github.com> Co-authored-by: Maggie <3593343+MaggieCabrera@users.noreply.github.com> Co-authored-by: Ben Dwyer <275961+scruffian@users.noreply.github.com> Co-authored-by: Ben Dwyer Co-authored-by: MaggieCabrera --- docs/reference-guides/core-blocks.md | 2 +- .../block-library/src/page-list-item/edit.js | 92 ++++++++++++++++++- .../block-library/src/page-list/block.json | 4 + packages/block-library/src/page-list/edit.js | 41 ++++++--- .../block-library/src/page-list/editor.scss | 4 + .../block-library/src/page-list/index.php | 12 ++- .../fixtures/blocks/core__page-list.json | 3 +- 7 files changed, 133 insertions(+), 25 deletions(-) diff --git a/docs/reference-guides/core-blocks.md b/docs/reference-guides/core-blocks.md index 14f800f140aee3..25fe5746a04122 100644 --- a/docs/reference-guides/core-blocks.md +++ b/docs/reference-guides/core-blocks.md @@ -429,7 +429,7 @@ Display a list of all pages. ([Source](https://github.com/WordPress/gutenberg/tr - **Name:** core/page-list - **Category:** widgets - **Supports:** typography (fontSize, lineHeight), ~~html~~, ~~reusable~~ -- **Attributes:** parentPageID +- **Attributes:** isNested, parentPageID ## Page List Item diff --git a/packages/block-library/src/page-list-item/edit.js b/packages/block-library/src/page-list-item/edit.js index db8e0a5c173736..aa0836e774848b 100644 --- a/packages/block-library/src/page-list-item/edit.js +++ b/packages/block-library/src/page-list-item/edit.js @@ -6,7 +6,7 @@ import classnames from 'classnames'; /** * WordPress dependencies */ -import { InnerBlocks } from '@wordpress/block-editor'; +import { useBlockProps, useInnerBlocksProps } from '@wordpress/block-editor'; import { useSelect } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; @@ -30,10 +30,95 @@ function useFrontPageId() { }, [] ); } +/** + * Determine the colors for a menu. + * + * Order of priority is: + * 1: Overlay custom colors (if submenu) + * 2: Overlay theme colors (if submenu) + * 3: Custom colors + * 4: Theme colors + * 5: Global styles + * + * @param {Object} context + * @param {boolean} isSubMenu + */ +function getColors( context, isSubMenu ) { + const { + textColor, + customTextColor, + backgroundColor, + customBackgroundColor, + overlayTextColor, + customOverlayTextColor, + overlayBackgroundColor, + customOverlayBackgroundColor, + style, + } = context; + + const colors = {}; + + if ( isSubMenu && !! customOverlayTextColor ) { + colors.customTextColor = customOverlayTextColor; + } else if ( isSubMenu && !! overlayTextColor ) { + colors.textColor = overlayTextColor; + } else if ( !! customTextColor ) { + colors.customTextColor = customTextColor; + } else if ( !! textColor ) { + colors.textColor = textColor; + } else if ( !! style?.color?.text ) { + colors.customTextColor = style.color.text; + } + + if ( isSubMenu && !! customOverlayBackgroundColor ) { + colors.customBackgroundColor = customOverlayBackgroundColor; + } else if ( isSubMenu && !! overlayBackgroundColor ) { + colors.backgroundColor = overlayBackgroundColor; + } else if ( !! customBackgroundColor ) { + colors.customBackgroundColor = customBackgroundColor; + } else if ( !! backgroundColor ) { + colors.backgroundColor = backgroundColor; + } else if ( !! style?.color?.background ) { + colors.customTextColor = style.color.background; + } + + return colors; +} + export default function PageListItemEdit( { context, attributes } ) { const { id, label, link, hasChildren } = attributes; const isNavigationChild = 'showSubmenuIcon' in context; const frontPageId = useFrontPageId(); + + const innerBlocksColors = getColors( context, true ); + + const blockProps = useBlockProps( { + className: classnames( + 'wp-block-pages-list__item', + 'wp-block-navigation__submenu-container', + { + 'has-text-color': !! ( + innerBlocksColors.textColor || + innerBlocksColors.customTextColor + ), + [ `has-${ innerBlocksColors.textColor }-color` ]: + !! innerBlocksColors.textColor, + 'has-background': !! ( + innerBlocksColors.backgroundColor || + innerBlocksColors.customBackgroundColor + ), + [ `has-${ innerBlocksColors.backgroundColor }-background-color` ]: + !! innerBlocksColors.backgroundColor, + } + ), + style: { + color: innerBlocksColors.customTextColor, + backgroundColor: innerBlocksColors.customBackgroundColor, + }, + } ); + + const innerBlocksProps = useInnerBlocksProps( blockProps ); + return (
  • - - + { ...innerBlocksProps } + > ) }
  • diff --git a/packages/block-library/src/page-list/block.json b/packages/block-library/src/page-list/block.json index e8ff316a9fb4f6..4f4f45c4bb4748 100644 --- a/packages/block-library/src/page-list/block.json +++ b/packages/block-library/src/page-list/block.json @@ -11,6 +11,10 @@ "parentPageID": { "type": "integer", "default": 0 + }, + "isNested": { + "type": "boolean", + "default": false } }, "usesContext": [ diff --git a/packages/block-library/src/page-list/edit.js b/packages/block-library/src/page-list/edit.js index 7372191723f466..219822ed87944f 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 } from '@wordpress/element'; +import { useMemo, useState, useEffect } from '@wordpress/element'; import { useEntityRecords } from '@wordpress/core-data'; import { useSelect, useDispatch } from '@wordpress/data'; @@ -189,20 +189,33 @@ export default function PageListEdit( { const { replaceBlock, selectBlock } = useDispatch( blockEditorStore ); - const { parentNavBlockClientId } = useSelect( ( select ) => { - const { getSelectedBlockClientId, getBlockParentsByBlockName } = - select( blockEditorStore ); - - const _selectedBlockClientId = getSelectedBlockClientId(); + const { parentNavBlockClientId, isNested } = useSelect( + ( select ) => { + const { getSelectedBlockClientId, getBlockParentsByBlockName } = + select( blockEditorStore ); + + const _selectedBlockClientId = getSelectedBlockClientId(); + + return { + parentNavBlockClientId: getBlockParentsByBlockName( + _selectedBlockClientId, + 'core/navigation', + true + )[ 0 ], + isNested: + getBlockParentsByBlockName( + clientId, + 'core/navigation-submenu', + true + ).length > 0, + }; + }, + [ clientId ] + ); - return { - parentNavBlockClientId: getBlockParentsByBlockName( - _selectedBlockClientId, - 'core/navigation', - true - )[ 0 ], - }; - }, [] ); + useEffect( () => { + setAttributes( { isNested } ); + }, [ isNested ] ); return ( <> diff --git a/packages/block-library/src/page-list/editor.scss b/packages/block-library/src/page-list/editor.scss index 83a70bafd94853..4cda6018220d32 100644 --- a/packages/block-library/src/page-list/editor.scss +++ b/packages/block-library/src/page-list/editor.scss @@ -22,6 +22,10 @@ } } +.wp-block-navigation .wp-block-navigation__submenu-container > .wp-block-page-list { + display: block; // This is needed to make sure the page list container is 100% wide, so that the children are correctly positioned. +} + // Make links unclickable in the editor. .wp-block-pages-list__item__link { pointer-events: none; diff --git a/packages/block-library/src/page-list/index.php b/packages/block-library/src/page-list/index.php index 7944f6f3a84ad6..3ae63047e27485 100644 --- a/packages/block-library/src/page-list/index.php +++ b/packages/block-library/src/page-list/index.php @@ -139,13 +139,14 @@ function block_core_page_list_build_css_font_sizes( $context ) { * @param boolean $show_submenu_icons Whether to show submenu indicator icons. * @param boolean $is_navigation_child If block is a child of Navigation block. * @param array $nested_pages The array of nested pages. + * @param boolean $is_nested Whether the submenu is nested or not. * @param array $active_page_ancestor_ids An array of ancestor ids for active page. * @param array $colors Color information for overlay styles. * @param integer $depth The nesting depth. * * @return string List markup. */ -function block_core_page_list_render_nested_page_list( $open_submenus_on_click, $show_submenu_icons, $is_navigation_child, $nested_pages, $active_page_ancestor_ids = array(), $colors = array(), $depth = 0 ) { +function block_core_page_list_render_nested_page_list( $open_submenus_on_click, $show_submenu_icons, $is_navigation_child, $nested_pages, $is_nested, $active_page_ancestor_ids = array(), $colors = array(), $depth = 0 ) { if ( empty( $nested_pages ) ) { return; } @@ -173,7 +174,7 @@ function block_core_page_list_render_nested_page_list( $open_submenus_on_click, $navigation_child_content_class = $is_navigation_child ? ' wp-block-navigation-item__content' : ''; // If this is the first level of submenus, include the overlay colors. - if ( 1 === $depth && isset( $colors['overlay_css_classes'], $colors['overlay_inline_styles'] ) ) { + if ( ( ( 0 < $depth && ! $is_nested ) || $is_nested ) && isset( $colors['overlay_css_classes'], $colors['overlay_inline_styles'] ) ) { $css_class .= ' ' . trim( implode( ' ', $colors['overlay_css_classes'] ) ); if ( '' !== $colors['overlay_inline_styles'] ) { $style_attribute = sprintf( ' style="%s"', esc_attr( $colors['overlay_inline_styles'] ) ); @@ -212,7 +213,7 @@ function block_core_page_list_render_nested_page_list( $open_submenus_on_click, if ( $is_navigation_child ) { $markup .= ' wp-block-navigation__submenu-container'; } - $markup .= '">' . block_core_page_list_render_nested_page_list( $open_submenus_on_click, $show_submenu_icons, $is_navigation_child, $page['children'], $active_page_ancestor_ids, $colors, $depth + 1 ) . ''; + $markup .= '">' . block_core_page_list_render_nested_page_list( $open_submenus_on_click, $show_submenu_icons, $is_navigation_child, $page['children'], $is_nested, $active_page_ancestor_ids, $colors, $depth + 1 ) . ''; } $markup .= ''; } @@ -253,6 +254,7 @@ function render_block_core_page_list( $attributes, $content, $block ) { ++$block_id; $parent_page_id = $attributes['parentPageID']; + $is_nested = $attributes['isNested']; $all_pages = get_pages( array( @@ -321,9 +323,9 @@ function render_block_core_page_list( $attributes, $content, $block ) { $show_submenu_icons = array_key_exists( 'showSubmenuIcon', $block->context ) ? $block->context['showSubmenuIcon'] : false; - $wrapper_markup = '
      %2$s
    '; + $wrapper_markup = $is_nested ? '%2$s' : '
      %2$s
    '; - $items_markup = block_core_page_list_render_nested_page_list( $open_submenus_on_click, $show_submenu_icons, $is_navigation_child, $nested_pages, $active_page_ancestor_ids, $colors ); + $items_markup = block_core_page_list_render_nested_page_list( $open_submenus_on_click, $show_submenu_icons, $is_navigation_child, $nested_pages, $is_nested, $active_page_ancestor_ids, $colors ); $wrapper_attributes = get_block_wrapper_attributes( array( diff --git a/test/integration/fixtures/blocks/core__page-list.json b/test/integration/fixtures/blocks/core__page-list.json index b1992a437b8849..2552d5e093c777 100644 --- a/test/integration/fixtures/blocks/core__page-list.json +++ b/test/integration/fixtures/blocks/core__page-list.json @@ -3,7 +3,8 @@ "name": "core/page-list", "isValid": true, "attributes": { - "parentPageID": 0 + "parentPageID": 0, + "isNested": false }, "innerBlocks": [] } From 76aa4bb2e9c8b5734e118823913963ef11c0a517 Mon Sep 17 00:00:00 2001 From: Nik Tsekouras Date: Fri, 16 Dec 2022 15:45:15 +0200 Subject: [PATCH 42/51] [Pattern Setup]: Fix full heights during transition. (#46615) --- .../src/components/block-pattern-setup/style.scss | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/block-editor/src/components/block-pattern-setup/style.scss b/packages/block-editor/src/components/block-pattern-setup/style.scss index 15edb5cd75f3ff..79f5868176608e 100644 --- a/packages/block-editor/src/components/block-pattern-setup/style.scss +++ b/packages/block-editor/src/components/block-pattern-setup/style.scss @@ -6,7 +6,6 @@ width: 100%; border-radius: $radius-block-ui; - // TODO change to check parent. &.view-mode-grid { .block-editor-block-pattern-setup__toolbar { justify-content: center; @@ -84,6 +83,7 @@ display: flex; flex-direction: column; width: 100%; + height: 100%; box-sizing: border-box; .carousel-container { @@ -91,6 +91,7 @@ position: relative; padding: 0; margin: 0; + height: 100%; list-style: none; transform-style: preserve-3d; * { @@ -100,6 +101,8 @@ position: absolute; top: 0; width: 100%; + height: 100%; + background-color: $white; margin: auto; padding: 0; transition: transform 0.5s, z-index 0.5s; From 9258e2dbfa8881944ccbd73e21dead650e5ba0ce Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Fri, 16 Dec 2022 14:23:15 +0000 Subject: [PATCH 43/51] Update: Use offsite navigation editor on the navigation inspector sidebar. (#46440) --- .../navigation-menu-sidebar/navigation-menu.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/edit-site/src/components/sidebar-edit-mode/navigation-menu-sidebar/navigation-menu.js b/packages/edit-site/src/components/sidebar-edit-mode/navigation-menu-sidebar/navigation-menu.js index 86e7d334318cc3..89a66bd6c73592 100644 --- a/packages/edit-site/src/components/sidebar-edit-mode/navigation-menu-sidebar/navigation-menu.js +++ b/packages/edit-site/src/components/sidebar-edit-mode/navigation-menu-sidebar/navigation-menu.js @@ -3,6 +3,7 @@ */ import { __experimentalListView as ListView, + __experimentalOffCanvasEditor as OffCanvasEditor, store as blockEditorStore, } from '@wordpress/block-editor'; import { useEffect } from '@wordpress/element'; @@ -48,5 +49,14 @@ export default function NavigationMenu( { innerBlocks, id } ) { } } ); }, [ updateBlockListSettings, innerBlocks ] ); + + if ( window?.__experimentalEnableOffCanvasNavigationEditor ) { + return ( + + ); + } return ; } From 869050027302d24b9ee6729c92d50f8fcb83e1f5 Mon Sep 17 00:00:00 2001 From: Jarda Snajdr Date: Fri, 16 Dec 2022 16:43:09 +0100 Subject: [PATCH 44/51] Element: export new React 18 APIs (#46610) * Element: export new React 18 APIs * Export more APIs --- packages/element/CHANGELOG.md | 4 ++ packages/element/README.md | 52 ++++++++++++++++++++++++++ packages/element/src/react-platform.js | 15 ++++++++ packages/element/src/react.js | 48 +++++++++++++++++++++--- 4 files changed, 113 insertions(+), 6 deletions(-) diff --git a/packages/element/CHANGELOG.md b/packages/element/CHANGELOG.md index efb51cc2dca060..78329b5047949f 100644 --- a/packages/element/CHANGELOG.md +++ b/packages/element/CHANGELOG.md @@ -6,6 +6,10 @@ - Updated dependencies to require React 18 ([45235](https://github.com/WordPress/gutenberg/pull/45235)) +### New Features + +- Started exporting new React 18 APIs ([46610](https://github.com/WordPress/gutenberg/pull/46610)) + ## 4.20.0 (2022-11-16) ## 4.19.0 (2022-11-02) diff --git a/packages/element/README.md b/packages/element/README.md index 8a3e39dc50a1ff..60a64113bca7ef 100755 --- a/packages/element/README.md +++ b/packages/element/README.md @@ -198,6 +198,14 @@ _Returns_ - `Object`: Ref object. +### createRoot + +Creates a new React root for the target DOM node. + +_Related_ + +- + ### findDOMNode Finds the dom node of a React component. @@ -234,6 +242,14 @@ _Parameters_ - _element_ `import('./react').WPElement`: Element to hydrate. - _target_ `HTMLElement`: DOM node into which element should be hydrated. +### hydrateRoot + +Creates a new React root for the target DOM node and hydrates it with a pre-generated markup. + +_Related_ + +- + ### isEmptyElement Checks if the provided WP element is empty. @@ -332,6 +348,12 @@ _Returns_ - `string`: Serialized element. +### startTransition + +_Related_ + +- + ### StrictMode Component that activates additional checks and warnings for its descendants. @@ -381,18 +403,36 @@ _Related_ - +### useDeferredValue + +_Related_ + +- + ### useEffect _Related_ - +### useId + +_Related_ + +- + ### useImperativeHandle _Related_ - +### useInsertionEffect + +_Related_ + +- + ### useLayoutEffect _Related_ @@ -423,6 +463,18 @@ _Related_ - +### useSyncExternalStore + +_Related_ + +- + +### useTransition + +_Related_ + +- + ## Contributing to this package diff --git a/packages/element/src/react-platform.js b/packages/element/src/react-platform.js index 7b35ea8d2a2d53..c819565b79553d 100644 --- a/packages/element/src/react-platform.js +++ b/packages/element/src/react-platform.js @@ -8,6 +8,7 @@ import { hydrate, unmountComponentAtNode, } from 'react-dom'; +import { createRoot, hydrateRoot } from 'react-dom/client'; /** * Creates a portal into which a component can be rendered. @@ -43,6 +44,20 @@ export { render }; */ export { hydrate }; +/** + * Creates a new React root for the target DOM node. + * + * @see https://reactjs.org/docs/react-dom-client.html#createroot + */ +export { createRoot }; + +/** + * Creates a new React root for the target DOM node and hydrates it with a pre-generated markup. + * + * @see https://reactjs.org/docs/react-dom-client.html#hydrateroot + */ +export { hydrateRoot }; + /** * Removes any mounted element from the target DOM node. * diff --git a/packages/element/src/react.js b/packages/element/src/react.js index 292f18cc92c627..4f2b8e150303ee 100644 --- a/packages/element/src/react.js +++ b/packages/element/src/react.js @@ -14,16 +14,22 @@ import { isValidElement, memo, StrictMode, - useState, - useEffect, - useContext, - useReducer, useCallback, + useContext, + useDebugValue, + useDeferredValue, + useEffect, + useId, useMemo, - useRef, useImperativeHandle, + useInsertionEffect, useLayoutEffect, - useDebugValue, + useReducer, + useRef, + useState, + useSyncExternalStore, + useTransition, + startTransition, lazy, Suspense, } from 'react'; @@ -150,16 +156,31 @@ export { useContext }; */ export { useDebugValue }; +/** + * @see https://reactjs.org/docs/hooks-reference.html#usedeferredvalue + */ +export { useDeferredValue }; + /** * @see https://reactjs.org/docs/hooks-reference.html#useeffect */ export { useEffect }; +/** + * @see https://reactjs.org/docs/hooks-reference.html#useid + */ +export { useId }; + /** * @see https://reactjs.org/docs/hooks-reference.html#useimperativehandle */ export { useImperativeHandle }; +/** + * @see https://reactjs.org/docs/hooks-reference.html#useinsertioneffect + */ +export { useInsertionEffect }; + /** * @see https://reactjs.org/docs/hooks-reference.html#uselayouteffect */ @@ -185,6 +206,21 @@ export { useRef }; */ export { useState }; +/** + * @see https://reactjs.org/docs/hooks-reference.html#usesyncexternalstore + */ +export { useSyncExternalStore }; + +/** + * @see https://reactjs.org/docs/hooks-reference.html#usetransition + */ +export { useTransition }; + +/** + * @see https://reactjs.org/docs/react-api.html#starttransition + */ +export { startTransition }; + /** * @see https://reactjs.org/docs/react-api.html#reactlazy */ From f19cbe2a49272ef20cf140976d53214833ff156e Mon Sep 17 00:00:00 2001 From: Maggie Date: Fri, 16 Dec 2022 16:51:37 +0100 Subject: [PATCH 45/51] Navigation list view: use smaller lock icon (#46578) * used smaller lock icon on off canvas editor * reduce size of lock icon on list view too * removed repeated css --- .../components/list-view/block-select-button.js | 2 +- .../src/components/list-view/style.scss | 7 ++++++- .../off-canvas-editor/block-select-button.js | 2 +- packages/icons/src/index.js | 1 + packages/icons/src/library/lock-small.js | 16 ++++++++++++++++ 5 files changed, 25 insertions(+), 3 deletions(-) create mode 100644 packages/icons/src/library/lock-small.js diff --git a/packages/block-editor/src/components/list-view/block-select-button.js b/packages/block-editor/src/components/list-view/block-select-button.js index 9477eb2cda40c0..39baf78d5a7a88 100644 --- a/packages/block-editor/src/components/list-view/block-select-button.js +++ b/packages/block-editor/src/components/list-view/block-select-button.js @@ -12,7 +12,7 @@ import { __experimentalTruncate as Truncate, } from '@wordpress/components'; import { forwardRef } from '@wordpress/element'; -import { Icon, lock } from '@wordpress/icons'; +import { Icon, lockSmall as lock } from '@wordpress/icons'; import { SPACE, ENTER } from '@wordpress/keycodes'; /** diff --git a/packages/block-editor/src/components/list-view/style.scss b/packages/block-editor/src/components/list-view/style.scss index 0c271352b91552..6bcb7667b36b85 100644 --- a/packages/block-editor/src/components/list-view/style.scss +++ b/packages/block-editor/src/components/list-view/style.scss @@ -319,11 +319,16 @@ .block-editor-list-view-block-select-button__lock { line-height: 0; - width: 24px; + width: 16px; min-width: 24px; + height: 16px; margin-left: auto; padding: 0; vertical-align: middle; + display: inline-flex; + justify-content: center; + align-items: center; + overflow: hidden; } } 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 a3cb64e9298dd4..543823ab1ada25 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 @@ -12,7 +12,7 @@ import { __experimentalTruncate as Truncate, } from '@wordpress/components'; import { forwardRef } from '@wordpress/element'; -import { Icon, lock } from '@wordpress/icons'; +import { Icon, lockSmall as lock } from '@wordpress/icons'; import { SPACE, ENTER } from '@wordpress/keycodes'; /** diff --git a/packages/icons/src/index.js b/packages/icons/src/index.js index 92bc48c185cc99..77c974fc61211f 100644 --- a/packages/icons/src/index.js +++ b/packages/icons/src/index.js @@ -129,6 +129,7 @@ export { default as listItem } from './library/list-item'; export { default as listView } from './library/list-view'; export { default as lock } from './library/lock'; export { default as lockOutline } from './library/lock-outline'; +export { default as lockSmall } from './library/lock-small'; export { default as login } from './library/login'; export { default as loop } from './library/loop'; export { default as mapMarker } from './library/map-marker'; diff --git a/packages/icons/src/library/lock-small.js b/packages/icons/src/library/lock-small.js new file mode 100644 index 00000000000000..9ed94cc264b003 --- /dev/null +++ b/packages/icons/src/library/lock-small.js @@ -0,0 +1,16 @@ +/** + * WordPress dependencies + */ +import { SVG, Path } from '@wordpress/primitives'; + +const lockSmall = ( + + + +); + +export default lockSmall; From a10e7eb16d14b7a83413dc35605213a877ca967a Mon Sep 17 00:00:00 2001 From: Kai Hao Date: Sat, 17 Dec 2022 02:03:44 +0800 Subject: [PATCH 46/51] Always run initialization code before e2e tests (#46459) --- packages/e2e-test-utils-playwright/src/test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/e2e-test-utils-playwright/src/test.ts b/packages/e2e-test-utils-playwright/src/test.ts index 3872ce5b9fafb4..6f8046f2dde7ea 100644 --- a/packages/e2e-test-utils-playwright/src/test.ts +++ b/packages/e2e-test-utils-playwright/src/test.ts @@ -153,7 +153,7 @@ const test = base.extend< await use( requestUtils ); }, - { scope: 'worker' }, + { scope: 'worker', auto: true }, ], // An automatic fixture to configure snapshot settings globally. snapshotConfig: [ From ec33daec0cefd421349eb2f1e3b739be3fef6acd Mon Sep 17 00:00:00 2001 From: Bart Kalisz Date: Fri, 16 Dec 2022 14:00:06 -0700 Subject: [PATCH 47/51] E2E: Refactor global inserter utility (#46366) --- packages/e2e-test-utils/README.md | 64 ++-- packages/e2e-test-utils/src/index.js | 13 +- packages/e2e-test-utils/src/inserter.js | 352 ++++++++++++------ .../plugins/block-directory-add.test.js | 2 +- 4 files changed, 279 insertions(+), 152 deletions(-) diff --git a/packages/e2e-test-utils/README.md b/packages/e2e-test-utils/README.md index 8d1ae7c47b1dfa..96ed7f3dcb1ea4 100644 --- a/packages/e2e-test-utils/README.md +++ b/packages/e2e-test-utils/README.md @@ -111,7 +111,7 @@ _Parameters_ ### closeGlobalBlockInserter -Undocumented declaration. +Closes the global inserter. ### closeListView @@ -455,41 +455,37 @@ _Returns_ ### insertBlock -Opens the inserter, searches for the given term, then selects the first -result that appears. It then waits briefly for the block list to update. +Inserts a block matching a given search term via the global inserter. _Parameters_ -- _searchTerm_ `string`: The text to search the inserter for. +- _searchTerm_ `string`: The term by which to find the block to insert. ### insertBlockDirectoryBlock -Opens the inserter, searches for the given block, then selects the -first result that appears from the block directory. It then waits briefly for the block list to -update. +Inserts a Block Directory block matching a given search term via the global +inserter. _Parameters_ -- _searchTerm_ `string`: The text to search the inserter for. +- _searchTerm_ `string`: The term by which to find the Block Directory block to insert. ### insertPattern -Opens the inserter, searches for the given pattern, then selects the first -result that appears. It then waits briefly for the block list to update. +Inserts a pattern matching a given search term via the global inserter. _Parameters_ -- _searchTerm_ `string`: The text to search the inserter for. +- _searchTerm_ `string`: The term by which to find the pattern to insert. ### insertReusableBlock -Opens the inserter, searches for the given reusable block, then selects the -first result that appears. It then waits briefly for the block list to -update. +Inserts a reusable block matching a given search term via the global +inserter. _Parameters_ -- _searchTerm_ `string`: The text to search the inserter for. +- _searchTerm_ `string`: The term by which to find the reusable block to insert. ### installPlugin @@ -577,7 +573,7 @@ Clicks on the button in the header which opens Document Settings sidebar when it ### openGlobalBlockInserter -Opens the global block inserter. +Opens the global inserter. ### openGlobalStylesPanel @@ -667,27 +663,51 @@ _Returns_ ### searchForBlock -Search for block in the global inserter +Searches for a block via the global inserter. + +_Parameters_ + +- _searchTerm_ `string`: The term to search the inserter for. + +_Returns_ + +- `Promise`: The handle of block to be inserted or null if nothing was found. + +### searchForBlockDirectoryBlock + +Searches for a Block Directory block via the global inserter. _Parameters_ -- _searchTerm_ `string`: The text to search the inserter for. +- _searchTerm_ `string`: The term to search the inserter for. + +_Returns_ + +- `Promise`: The handle of the Block Directory block to be inserted or null if nothing was found. ### searchForPattern -Search for pattern in the global inserter +Searches for a pattern via the global inserter. _Parameters_ -- _searchTerm_ `string`: The text to search the inserter for. +- _searchTerm_ `string`: The term to search the inserter for. + +_Returns_ + +- `Promise`: The handle of the pattern to be inserted or null if nothing was found. ### searchForReusableBlock -Search for reusable block in the global inserter. +Searches for a reusable block via the global inserter. _Parameters_ -- _searchTerm_ `string`: The text to search the inserter for. +- _searchTerm_ `string`: The term to search the inserter for. + +_Returns_ + +- `Promise`: The handle of the reusable block to be inserted or null if nothing was found. ### selectBlockByClientId diff --git a/packages/e2e-test-utils/src/index.js b/packages/e2e-test-utils/src/index.js index fda35292174b00..6b217d52c44074 100644 --- a/packages/e2e-test-utils/src/index.js +++ b/packages/e2e-test-utils/src/index.js @@ -42,16 +42,17 @@ export { hasBlockSwitcher } from './has-block-switcher'; export { getPageError } from './get-page-error'; export { getOption } from './get-option'; export { - insertBlock, - insertPattern, - insertReusableBlock, + openGlobalBlockInserter, + closeGlobalBlockInserter, + toggleGlobalBlockInserter, searchForBlock, searchForPattern, searchForReusableBlock, + searchForBlockDirectoryBlock, + insertBlock, + insertPattern, + insertReusableBlock, insertBlockDirectoryBlock, - openGlobalBlockInserter, - closeGlobalBlockInserter, - toggleGlobalBlockInserter, } from './inserter'; export { installPlugin } from './install-plugin'; export { installTheme } from './install-theme'; diff --git a/packages/e2e-test-utils/src/inserter.js b/packages/e2e-test-utils/src/inserter.js index e69282c9dd2bf5..84521728160bee 100644 --- a/packages/e2e-test-utils/src/inserter.js +++ b/packages/e2e-test-utils/src/inserter.js @@ -1,3 +1,8 @@ +/** + * External dependencies + */ +import { ElementHandle } from 'puppeteer-core'; + /** * Internal dependencies */ @@ -10,38 +15,37 @@ const INSERTER_SEARCH_SELECTOR = '.block-editor-inserter__search input,.block-editor-inserter__search-input,input.block-editor-inserter__search'; /** - * Opens the global block inserter. + * Opens the global inserter. */ export async function openGlobalBlockInserter() { - if ( await isGlobalInserterOpen() ) { - // If global inserter is already opened, reset to an initial state where - // the default (first) tab is selected. - const tab = await page.$( - '.block-editor-inserter__tabs .components-tab-panel__tabs-item:nth-of-type(1):not(.is-active)' - ); - - if ( tab ) { - await tab.click(); - } - } else { + if ( ! ( await isGlobalInserterOpen() ) ) { await toggleGlobalBlockInserter(); - // Waiting here is necessary because sometimes the inserter takes more time to - // render than Puppeteer takes to complete the 'click' action. + // Waiting here is necessary because sometimes the inserter takes more + // time to render than Puppeteer takes to complete the 'click' action. await page.waitForSelector( '.block-editor-inserter__menu' ); } } +/** + * Closes the global inserter. + */ export async function closeGlobalBlockInserter() { if ( await isGlobalInserterOpen() ) { await toggleGlobalBlockInserter(); } } +/** + * Checks if the global inserter is open. + * + * @return {Promise} Whether the inserter is open or not. + */ async function isGlobalInserterOpen() { return await page.evaluate( () => { // "Add block" selector is required to make sure performance comparison - // doesn't fail on older branches where we still had "Add block" as label. + // doesn't fail on older branches where we still had "Add block" as + // label. return !! document.querySelector( '.edit-post-header [aria-label="Add block"].is-pressed,' + '.edit-site-header-edit-mode [aria-label="Add block"].is-pressed,' + @@ -70,11 +74,56 @@ export async function toggleGlobalBlockInserter() { ); } +/** + * Selects the global inserter tab/category, unless it's already selected. + * + * @param {string} label The label of the tab to select. + */ +export async function selectGlobalInserterTab( label ) { + const tabs = await page.$( '.block-editor-inserter__tabs' ); + if ( ! tabs ) { + return; // Do nothing if tabs are unavailable (e.g. for inner blocks). + } + + const activeTab = await page.waitForSelector( + '.block-editor-inserter__tabs button.is-active' + ); + + const activeTabLabel = await page.evaluate( + ( el ) => el.innerText, + activeTab + ); + + if ( activeTabLabel === label ) { + return; // Do nothing if the target tab is already active. + } + + let labelSelector; + + switch ( label ) { + case 'Blocks': + case 'Patterns': + case 'Media': + labelSelector = `. = "${ label }"`; + break; + case 'Reusable': + // Reusable tab label is an icon, hence the different selector. + labelSelector = `@aria-label = "${ label }"`; + break; + } + + const targetTab = await page.waitForXPath( + `//div[contains(@class, "block-editor-inserter__tabs")]//button[${ labelSelector }]` + ); + + await targetTab.click(); +} + /** * Moves focus to the selected block. */ async function focusSelectedBlock() { - // Ideally there shouuld be a UI way to do this. (Focus the selected block) + // Ideally, there should be a UI way to focus the selected block. await page.evaluate( () => { wp.data .dispatch( 'core/block-editor' ) @@ -88,7 +137,8 @@ async function focusSelectedBlock() { } /** - * Retrieves the document container by css class and checks to make sure the document's active element is within it + * Retrieves the document container by css class and checks to make sure the + * document's active element is within it. */ async function waitForInserterCloseAndContentFocus() { await canvas().waitForFunction( @@ -100,157 +150,213 @@ async function waitForInserterCloseAndContentFocus() { } /** - * Wait for the inserter search to yield results because that input is debounced. + * Searches for an entity matching given category and term via the global + * inserter. If nothing is found, null will be returned. + * + * Available categories: Blocks, Patterns, Reusable and Block Directory. + * + * @param {string} category The category to search within. + * @param {string} searchTerm The term to search the inserter for. + * @return {Promise} The handle of the element to be + * inserted or null if nothing was found. + */ +export async function searchGlobalInserter( category, searchTerm ) { + await openGlobalBlockInserter(); + await page.waitForSelector( INSERTER_SEARCH_SELECTOR ); + await page.focus( INSERTER_SEARCH_SELECTOR ); + await pressKeyWithModifier( 'primary', 'a' ); + await page.keyboard.type( searchTerm ); + + // Wait for the default block list to disappear to prevent its items from + // being considered as search results. This is needed since we're debouncing + // search request. + await page.waitForSelector( '.block-editor-inserter__block-list', { + hidden: true, + } ); + + let waitForInsertElement; + let waitForNoResults; + + switch ( category ) { + case 'Blocks': + case 'Patterns': + case 'Reusable': { + waitForInsertElement = async () => { + return await page.waitForXPath( + `//*[@role='option' and contains(., '${ searchTerm }')]` + ); + }; + waitForNoResults = async () => { + await page.waitForSelector( + '.block-editor-inserter__no-results' + ); + return null; + }; + break; + } + case 'Block Directory': { + waitForInsertElement = async () => { + // Return the first item from the Block Directory search results. + return await page.waitForSelector( + '.block-directory-downloadable-blocks-list button:first-child' + ); + }; + waitForNoResults = async () => { + // Use a soft timeout if Block Directory doesn't return anything + // within 5 seconds, as there's no "empty results" element being + // rendered when nothing is found. + return await new Promise( ( resolve ) => + setTimeout( () => resolve( null ), 5000 ) + ); + }; + } + } + + return await Promise.race( [ waitForInsertElement(), waitForNoResults() ] ); +} + +/** + * Inserts an entity matching given category and term via the global inserter. + * If the entity is not instantly available in the open inserter, a search will + * be performed. If the search returns no results, an error will be thrown. + * + * Available categories: Blocks, Patterns, Reusable and Block Directory. + * + * @param {string} category The category to insert from. + * @param {string} searchTerm The term by which to find the entity to insert. */ -async function waitForInserterSearch() { - try { +export async function insertFromGlobalInserter( category, searchTerm ) { + await openGlobalBlockInserter(); + await selectGlobalInserterTab( category ); + + let insertButton; + + if ( [ 'Blocks', 'Reusable' ].includes( category ) ) { + // If it's a block, see it it's insertable without searching... + try { + insertButton = ( + await page.$x( + `//*[@role='option' and contains(., '${ searchTerm }')]` + ) + )[ 0 ]; + } catch ( error ) { + // noop + } + } + + // ...and if not, perform a global search. + if ( ! insertButton ) { + insertButton = await searchGlobalInserter( category, searchTerm ); + } + + // Throw an error if nothing was found. + if ( ! insertButton ) { + throw new Error( + `Couldn't find "${ searchTerm }" in the ${ category } category.` + ); + } + + // Insert found entity. + await insertButton.click(); + + // Extra wait for the reusable block to be ready. + if ( category === 'Reusable' ) { await page.waitForSelector( - '.block-editor-inserter__no-tab-container', - { timeout: 2000 } + '.block-library-block__reusable-block-container' ); - } catch ( e ) { - // This selector doesn't exist in older versions, so let's just continue. } + + // Extra wait for the Block Directory block to be ready. + if ( category === 'Block Directory' ) { + await page.waitForSelector( + '.block-directory-downloadable-blocks-list button:first-child:not(.is-busy)' + ); + } + + await focusSelectedBlock(); + await waitForInserterCloseAndContentFocus(); } /** - * Search for block in the global inserter + * Searches for a block via the global inserter. * - * @param {string} searchTerm The text to search the inserter for. + * @param {string} searchTerm The term to search the inserter for. + * @return {Promise} The handle of block to be + * inserted or null if nothing was found. */ export async function searchForBlock( searchTerm ) { - await openGlobalBlockInserter(); - await page.waitForSelector( INSERTER_SEARCH_SELECTOR ); - await page.focus( INSERTER_SEARCH_SELECTOR ); - await pressKeyWithModifier( 'primary', 'a' ); - await page.keyboard.type( searchTerm ); - await waitForInserterSearch(); + return await searchGlobalInserter( 'Blocks', searchTerm ); } /** - * Search for pattern in the global inserter + * Searches for a pattern via the global inserter. * - * @param {string} searchTerm The text to search the inserter for. + * @param {string} searchTerm The term to search the inserter for. + * @return {Promise} The handle of the pattern to be + * inserted or null if nothing was found. */ export async function searchForPattern( searchTerm ) { - await openGlobalBlockInserter(); - // Select the patterns tab. - const tab = await page.waitForXPath( - '//div[contains(@class, "block-editor-inserter__tabs")]//button[.="Patterns"]' - ); - await tab.click(); - await page.waitForSelector( INSERTER_SEARCH_SELECTOR ); - await page.focus( INSERTER_SEARCH_SELECTOR ); - await pressKeyWithModifier( 'primary', 'a' ); - await page.keyboard.type( searchTerm ); - await waitForInserterSearch(); + return await searchGlobalInserter( 'Patterns', searchTerm ); } /** - * Search for reusable block in the global inserter. + * Searches for a reusable block via the global inserter. * - * @param {string} searchTerm The text to search the inserter for. + * @param {string} searchTerm The term to search the inserter for. + * @return {Promise} The handle of the reusable block to be + * inserted or null if nothing was found. */ export async function searchForReusableBlock( searchTerm ) { - await openGlobalBlockInserter(); - - // The reusable blocks tab won't appear until the reusable blocks have been - // fetched. They aren't fetched until an inserter is used or the post - // already contains reusable blocks, so wait for the tab to appear. - await page.waitForXPath( - '//div[contains(@class, "block-editor-inserter__tabs")]//button[@aria-label="Reusable"]' - ); + return await searchGlobalInserter( 'Reusable', searchTerm ); +} - // Select the reusable blocks tab. - const tab = await page.waitForXPath( - '//div[contains(@class, "block-editor-inserter__tabs")]//button[@aria-label="Reusable"]' - ); - await tab.click(); - await page.waitForSelector( INSERTER_SEARCH_SELECTOR ); - await page.focus( INSERTER_SEARCH_SELECTOR ); - await pressKeyWithModifier( 'primary', 'a' ); - await page.keyboard.type( searchTerm ); - await waitForInserterSearch(); +/** + * Searches for a Block Directory block via the global inserter. + * + * @param {string} searchTerm The term to search the inserter for. + * @return {Promise} The handle of the Block Directory block + * to be inserted or null if nothing was found. + */ +export async function searchForBlockDirectoryBlock( searchTerm ) { + return await searchGlobalInserter( 'Block Directory', searchTerm ); } /** - * Opens the inserter, searches for the given term, then selects the first - * result that appears. It then waits briefly for the block list to update. + * Inserts a block matching a given search term via the global inserter. * - * @param {string} searchTerm The text to search the inserter for. + * @param {string} searchTerm The term by which to find the block to insert. */ export async function insertBlock( searchTerm ) { - await searchForBlock( searchTerm ); - const insertButton = await page.waitForXPath( - `//button//span[contains(text(), '${ searchTerm }')]` - ); - await insertButton.click(); - await focusSelectedBlock(); - // We should wait until the inserter closes and the focus moves to the content. - await waitForInserterCloseAndContentFocus(); + await insertFromGlobalInserter( 'Blocks', searchTerm ); } /** - * Opens the inserter, searches for the given pattern, then selects the first - * result that appears. It then waits briefly for the block list to update. + * Inserts a pattern matching a given search term via the global inserter. * - * @param {string} searchTerm The text to search the inserter for. + * @param {string} searchTerm The term by which to find the pattern to insert. */ export async function insertPattern( searchTerm ) { - await searchForPattern( searchTerm ); - const insertButton = await page.waitForXPath( - `//div[@role = 'option']//div[contains(text(), '${ searchTerm }')]` - ); - await insertButton.click(); - await focusSelectedBlock(); - // We should wait until the inserter closes and the focus moves to the content. - await waitForInserterCloseAndContentFocus(); + await insertFromGlobalInserter( 'Patterns', searchTerm ); } /** - * Opens the inserter, searches for the given reusable block, then selects the - * first result that appears. It then waits briefly for the block list to - * update. + * Inserts a reusable block matching a given search term via the global + * inserter. * - * @param {string} searchTerm The text to search the inserter for. + * @param {string} searchTerm The term by which to find the reusable block to + * insert. */ export async function insertReusableBlock( searchTerm ) { - await searchForReusableBlock( searchTerm ); - const insertButton = await page.waitForXPath( - `//button//span[contains(text(), '${ searchTerm }')]` - ); - await insertButton.click(); - await focusSelectedBlock(); - // We should wait until the inserter closes and the focus moves to the content. - await waitForInserterCloseAndContentFocus(); - // We should wait until the block is loaded. - await page.waitForXPath( - '//*[contains(@class,"block-library-block__reusable-block-container")]' - ); + await insertFromGlobalInserter( 'Reusable', searchTerm ); } /** - * Opens the inserter, searches for the given block, then selects the - * first result that appears from the block directory. It then waits briefly for the block list to - * update. + * Inserts a Block Directory block matching a given search term via the global + * inserter. * - * @param {string} searchTerm The text to search the inserter for. + * @param {string} searchTerm The term by which to find the Block Directory + * block to insert. */ export async function insertBlockDirectoryBlock( searchTerm ) { - await searchForBlock( searchTerm ); - - // Grab the first block in the list. - const insertButton = await page.waitForSelector( - '.block-directory-downloadable-blocks-list button:first-child' - ); - await insertButton.click(); - await page.waitForFunction( - () => - ! document.body.querySelector( - '.block-directory-downloadable-blocks-list button:first-child.is-busy' - ) - ); - await focusSelectedBlock(); - // We should wait until the inserter closes and the focus moves to the content. - await waitForInserterCloseAndContentFocus(); + return await insertFromGlobalInserter( 'Block Directory', searchTerm ); } diff --git a/packages/e2e-tests/specs/editor/plugins/block-directory-add.test.js b/packages/e2e-tests/specs/editor/plugins/block-directory-add.test.js index d1537263968192..2e969d17915924 100644 --- a/packages/e2e-tests/specs/editor/plugins/block-directory-add.test.js +++ b/packages/e2e-tests/specs/editor/plugins/block-directory-add.test.js @@ -166,7 +166,7 @@ const matchUrl = ( reqUrl, urls ) => { }; describe( 'adding blocks from block directory', () => { - beforeEach( async () => { + beforeAll( async () => { await createNewPost(); } ); From 4b4c4befb34d815634b85cbee23cad169ab0e073 Mon Sep 17 00:00:00 2001 From: brookewp <35543432+brookewp@users.noreply.github.com> Date: Fri, 16 Dec 2022 13:14:39 -0800 Subject: [PATCH 48/51] TextareaControl: add new opt-in prop (#46559) * TextareaControl: add new opt-in prop * Remove margin bottom and replace div with VStack for Disabled component * Update Changelog * Revert "Update Changelog" --- .../block-library/src/cover/edit/inspector-controls.js | 1 + packages/block-library/src/image/image.js | 1 + packages/block-library/src/media-text/edit.js | 1 + packages/block-library/src/navigation-link/edit.js | 1 + packages/block-library/src/navigation-submenu/edit.js | 1 + packages/components/src/disabled/stories/index.tsx | 8 ++++++-- .../edit-site/src/components/global-styles/custom-css.js | 1 + packages/editor/src/components/post-excerpt/index.js | 1 + 8 files changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/block-library/src/cover/edit/inspector-controls.js b/packages/block-library/src/cover/edit/inspector-controls.js index d3f65672e09328..6160f5d7828d6a 100644 --- a/packages/block-library/src/cover/edit/inspector-controls.js +++ b/packages/block-library/src/cover/edit/inspector-controls.js @@ -180,6 +180,7 @@ export default function CoverInspectorControls( { isImageBackground && isImgElement && ( { ! multiImageSelection && ( { setAttributes( { description: descriptionValue } ); diff --git a/packages/block-library/src/navigation-submenu/edit.js b/packages/block-library/src/navigation-submenu/edit.js index 47a222bd57bb66..a6029e98661ddc 100644 --- a/packages/block-library/src/navigation-submenu/edit.js +++ b/packages/block-library/src/navigation-submenu/edit.js @@ -481,6 +481,7 @@ export default function NavigationSubmenuEdit( { autoComplete="off" /> { setAttributes( { diff --git a/packages/components/src/disabled/stories/index.tsx b/packages/components/src/disabled/stories/index.tsx index 06246adde633f1..81f2bf4c079233 100644 --- a/packages/components/src/disabled/stories/index.tsx +++ b/packages/components/src/disabled/stories/index.tsx @@ -15,6 +15,7 @@ import Disabled from '../'; import SelectControl from '../../select-control/'; import TextControl from '../../text-control/'; import TextareaControl from '../../textarea-control/'; +import { VStack } from '../../v-stack/'; const meta: ComponentMeta< typeof Disabled > = { title: 'Components/Disabled', @@ -37,18 +38,21 @@ const Form = () => { const [ textControlValue, setTextControlValue ] = useState( '' ); const [ textAreaValue, setTextAreaValue ] = useState( '' ); return ( -
    + {} } options={ [ @@ -58,7 +62,7 @@ const Form = () => { { value: 'c', label: 'Option C' }, ] } /> -
    + ); }; diff --git a/packages/edit-site/src/components/global-styles/custom-css.js b/packages/edit-site/src/components/global-styles/custom-css.js index 6c7d3de1cf06e5..61aad940cd372f 100644 --- a/packages/edit-site/src/components/global-styles/custom-css.js +++ b/packages/edit-site/src/components/global-styles/custom-css.js @@ -42,6 +42,7 @@ function CustomCSSControl() { return ( <> onUpdateExcerpt( value ) } From 69388d0301b198239fe9062e0628ce4dffd08c7a Mon Sep 17 00:00:00 2001 From: Viral Sampat <53076293+viralsampat-multidots@users.noreply.github.com> Date: Sun, 18 Dec 2022 22:13:48 +0530 Subject: [PATCH 49/51] fixed table block footer section issue (#46567) * fixed table block footer section issue * fixed table block footer section issue * fixed table block footer section issue and removed extra css from theme file --- packages/block-library/src/table/style.scss | 8 ++++++++ packages/block-library/src/table/theme.scss | 8 -------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/block-library/src/table/style.scss b/packages/block-library/src/table/style.scss index 9721513a53eee0..4f4271650f50e1 100644 --- a/packages/block-library/src/table/style.scss +++ b/packages/block-library/src/table/style.scss @@ -11,6 +11,14 @@ width: 100%; } + thead { + border-bottom: 3px solid; + } + + tfoot { + border-top: 3px solid; + } + // Match default border style to default style in editor td, th { diff --git a/packages/block-library/src/table/theme.scss b/packages/block-library/src/table/theme.scss index 3adc4d3e452ba7..d42e79b02b4965 100644 --- a/packages/block-library/src/table/theme.scss +++ b/packages/block-library/src/table/theme.scss @@ -1,14 +1,6 @@ .wp-block-table { margin: 0 0 1em 0; - thead { - border-bottom: 3px solid; - } - - tfoot { - border-top: 3px solid; - } - td, th { word-break: normal; From ded93540d8b333570f4968a119157894f0619d96 Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Mon, 19 Dec 2022 11:55:43 +0800 Subject: [PATCH 50/51] Verse: Prevent default styles overriding theme.json font family (#46560) --- packages/block-library/src/verse/style.scss | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/block-library/src/verse/style.scss b/packages/block-library/src/verse/style.scss index 954e6abaa517d4..2b3247f0b40cdc 100644 --- a/packages/block-library/src/verse/style.scss +++ b/packages/block-library/src/verse/style.scss @@ -1,5 +1,8 @@ pre.wp-block-verse { - font-family: inherit; overflow: auto; white-space: pre-wrap; } + +:where(pre.wp-block-verse) { + font-family: inherit; +} From f6cccc29db4f474db9809933078928b405a2d820 Mon Sep 17 00:00:00 2001 From: Ramon Date: Mon, 19 Dec 2022 18:07:23 +1100 Subject: [PATCH 51/51] Reverting changes and updated doc comment blocks. (#43928) The reason for revert is that `wp_typography_get_css_variable_inline_style` was only deprecated in 6.1, and the Gutenberg plugin supports a minimum of 6.0. --- lib/block-supports/typography.php | 35 +++++++++++++++++++------------ 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/lib/block-supports/typography.php b/lib/block-supports/typography.php index 809fba1a6e7aca..aa6c74a5d2e73c 100644 --- a/lib/block-supports/typography.php +++ b/lib/block-supports/typography.php @@ -168,16 +168,21 @@ function gutenberg_apply_typography_support( $block_type, $block_attributes ) { } /** - * Note: this method is for backwards compatibility. - * It mostly replaces `gutenberg_typography_get_css_variable_inline_style()`. - * * Generates an inline style value for a typography feature e.g. text decoration, * text transform, and font style. * - * @param string $style_value A raw style value for a single typography feature from a block's style attribute. - * @param string $css_property Slug for the CSS property the inline style sets. + * Note: This function is for backwards compatibility. + * * It is necessary to parse older blocks whose typography styles contain presets. + * * It mostly replaces the deprecated `wp_typography_get_css_variable_inline_style()`, + * but skips compiling a CSS declaration as the style engine takes over this role. + * + * @link https://github.com/wordpress/gutenberg/pull/27555 + * + * @since 6.1.0 * - * @return string? A CSS inline style value. + * @param string $style_value A raw style value for a single typography feature from a block's style attribute. + * @param string $css_property Slug for the CSS property the inline style sets. + * @return string A CSS inline style value. */ function gutenberg_typography_get_preset_inline_style_value( $style_value, $css_property ) { // If the style value is not a preset CSS variable go no further. @@ -185,10 +190,12 @@ function gutenberg_typography_get_preset_inline_style_value( $style_value, $css_ return $style_value; } - // For backwards compatibility. - // Presets were removed in https://github.com/WordPress/gutenberg/pull/27555. - // We have a preset CSS variable as the style. - // Get the style value from the string and return CSS style. + /* + * For backwards compatibility. + * Presets were removed in WordPress/gutenberg#27555. + * We have a preset CSS variable as the style. + * Get the style value from the string and return CSS style. + */ $index_to_splice = strrpos( $style_value, '|' ) + 1; $slug = _wp_to_kebab_case( substr( $style_value, $index_to_splice ) ); @@ -197,14 +204,16 @@ function gutenberg_typography_get_preset_inline_style_value( $style_value, $css_ } /** - * Deprecated. - * This method is no longer used and will have to be deprecated in Core. + * This method is no longer used and has been deprecated in Core since 6.1.0. * - * It can be deleted once migrated to the next WordPress version. + * It can be deleted once Gutenberg's minimum supported WordPress version is >= 6.1 * * Generates an inline style for a typography feature e.g. text decoration, * text transform, and font style. * + * @since 5.8.0 + * @deprecated 6.1.0 + * * @param array $attributes Block's attributes. * @param string $feature Key for the feature within the typography styles. * @param string $css_property Slug for the CSS property the inline style sets.