From 8c58423687b7ccdeaf50f5ae338fd9cf75cdc369 Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Tue, 13 Feb 2024 11:04:42 +0100 Subject: [PATCH 01/78] Add contributing guidlines around Component versioning (#58789) * Add contributing guidlines around Component versioning * Fix typo --------- Co-authored-by: ciampo Co-authored-by: mirka <0mirka00@git.wordpress.org> --- packages/components/CONTRIBUTING.md | 72 +++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/packages/components/CONTRIBUTING.md b/packages/components/CONTRIBUTING.md index c948a8d389d68b..ef9b393dbffd21 100644 --- a/packages/components/CONTRIBUTING.md +++ b/packages/components/CONTRIBUTING.md @@ -19,6 +19,7 @@ For an example of a component that follows these requirements, take a look at [` - [Documentation](#documentation) - [README example](#README-example) - [Folder structure](#folder-structure) +- [Component versioning](#component-versioning) ## Introducing new components @@ -591,3 +592,74 @@ component-family-name/ ├── types.ts └── utils.ts ``` + +## Component versioning + +As the needs of the package evolve with time, sometimes we may opt to fully rewrite an existing component — either to introduce substantial changes, support new features, or swap the implementation details. + +### Glossary + +Here is some terminology that will be used in the upcoming sections: + +- "Legacy" component: the version(s) of the component that existsted on `trunk` before the rewrite; +- API surface: the component's public APIs. It includes the list of components (and sub-components) exported from the package, their props, any associated React context. It does not include internal classnames and internal DOM structure of the components. + +### Approaches + +We identified two approaches to the task. + +#### Swap the implementation, keep the same API surface + +One possible approach is to keep the existing API surface and only swap the internal implementation of the component. + +This is by far the simplest approach, since it doesn't involve making changes to the API surface. + +If the existing API surface is not a good fit for the new implementation, or if it is not possible (or simply not desirable) to preserve backward compatibility with the existing implementation, there is another approach that can be used. + +#### Create a new component (or component family) + +This second approach involves creating a new, separate version (ie. export) of the component. Having two separate exports will help to keep the package tree-shakeable, and it will make it easier to potentially deprecated and remove the legacy component. + +If possible, the legacy version of the component should be rewritten so that it uses the same underlying implementation of the new version, with an extra API "translation" layer to adapt the legacy API surface to the new API surface, e.g: + +``` +// legacy-component/index.tsx + +function LegacyComponent( props ) { + const newProps = useTranslateLegacyPropsToNewProps( props ); + + return ( ); +} + +// new-component/index.tsx +function NewComponent( props ) { + return ( ); +} + +// new-component/implementation.tsx +function NewComponentImplementation( props ) { + // implementation +} + +``` + +In case that is not possible (eg. too difficult to reconciliate new and legacy implementations, or impossible to preserve backward compatibility), then the legacy implementation can stay as-is. + +In any case, extra attention should be payed to legacy component families made of two or more subcomponents. It is possible, in fact, that the a legacy subcomponent is used as a parent / child of a subcomponent from the new version (this can happen, for example, when Gutenberg allows third party developers to inject React components via Slot/Fill). To avoid incompatibility issues and unexpected behavior, there should be some code in the components warning when the above scenario happens — or even better, aliasing to the correct version of the component. + +##### Naming + +When it comes to naming the newly added component, there are two options. + +If there is a good reason for it, pick a new name for the component. For example, some legacy components have names that don't correspond to the corrent name of UI widget that they implement (for example, `TabPanel` should be called `Tabs`, and `Modal` should be called `Dialog`). + +Alternatively, version the component name. For example, the new version of `Component` could be called `ComponentV2`. This also applies for namespaced subcomponents (ie. `ComponentV2.SubComponent`). + +### Methodology + +Regardless of the chosen approach, we recommend adopting the following methodology: + +1. First, make sure that the legacy component is well covered by automated tests. Using those tests against the new implementation will serve as a great first layer to make sure that we don't break backward compatibility where necessary, and that we are otherwise aware of any differences in behavior; +2. Create a new temporary folder, so that all the work can be done without affecting publicly exported APIs; make it explicit in the README, JSDocs and Storybook (by using badges) that the components are WIP and shouldn't be used outside of the components package; +3. Once the first iteration of the new component(s) is complete, start testing it by exporting it via private APIs, and replacing usages of the legacy component across the Gutenberg repository. This process is great to gather more feedback, spot bugs and missing features; +4. Once all usages are migrated, you can replace the legacy component with the new implementation, and delete the temporary folder and private exports. Don't forget to write a dev note when necessary. From 3f249471bf8bfed5cb0f9a7a15dd3b43bf3df464 Mon Sep 17 00:00:00 2001 From: tellthemachines Date: Tue, 13 Feb 2024 21:48:14 +1100 Subject: [PATCH 02/78] Update test environment default theme versions to latest. (#58955) --- .wp-env.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.wp-env.json b/.wp-env.json index 90457c8da459ec..20d5597e54bbc9 100644 --- a/.wp-env.json +++ b/.wp-env.json @@ -9,8 +9,8 @@ "wp-content/mu-plugins": "./packages/e2e-tests/mu-plugins", "wp-content/plugins/gutenberg-test-plugins": "./packages/e2e-tests/plugins", "wp-content/themes/gutenberg-test-themes": "./test/gutenberg-test-themes", - "wp-content/themes/gutenberg-test-themes/twentytwentyone": "https://downloads.wordpress.org/theme/twentytwentyone.1.7.zip", - "wp-content/themes/gutenberg-test-themes/twentytwentythree": "https://downloads.wordpress.org/theme/twentytwentythree.1.0.zip", + "wp-content/themes/gutenberg-test-themes/twentytwentyone": "https://downloads.wordpress.org/theme/twentytwentyone.2.1.zip", + "wp-content/themes/gutenberg-test-themes/twentytwentythree": "https://downloads.wordpress.org/theme/twentytwentythree.1.3.zip", "wp-content/themes/gutenberg-test-themes/twentytwentyfour": "https://downloads.wordpress.org/theme/twentytwentyfour.1.0.zip" } } From 5bd036b2c982d774ce69fcef59849a70220c91ba Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Tue, 13 Feb 2024 11:56:44 +0100 Subject: [PATCH 03/78] Fix layout for non viewable post types (#58962) Co-authored-by: youknowriad Co-authored-by: glendaviesnz Co-authored-by: t-hamano --- .../src/components/editor-canvas/index.js | 29 +++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/packages/editor/src/components/editor-canvas/index.js b/packages/editor/src/components/editor-canvas/index.js index f011f285644c0e..bc7d54583afbda 100644 --- a/packages/editor/src/components/editor-canvas/index.js +++ b/packages/editor/src/components/editor-canvas/index.js @@ -40,6 +40,17 @@ const { const noop = () => {}; +/** + * These post types have a special editor where they don't allow you to fill the title + * and they don't apply the layout styles. + */ +const DESIGN_POST_TYPES = [ + 'wp_block', + 'wp_template', + 'wp_navigation', + 'wp_template_part', +]; + /** * Given an array of nested blocks, find the first Post Content * block inside it, recursing through any nesting levels, @@ -93,6 +104,7 @@ function EditorCanvas( { wrapperUniqueId, deviceType, showEditorPadding, + isDesignPostType, } = useSelect( ( select ) => { const { getCurrentPostId, @@ -130,6 +142,7 @@ function EditorCanvas( { return { renderingMode: _renderingMode, postContentAttributes: editorSettings.postContentAttributes, + isDesignPostType: DESIGN_POST_TYPES.includes( postTypeSlug ), // Post template fetch returns a 404 on classic themes, which // messes with e2e tests, so check it's a block theme first. editedPostTemplate: @@ -164,7 +177,7 @@ function EditorCanvas( { // fallbackLayout is used if there is no Post Content, // and for Post Title. const fallbackLayout = useMemo( () => { - if ( renderingMode !== 'post-only' ) { + if ( renderingMode !== 'post-only' || isDesignPostType ) { return { type: 'default' }; } @@ -175,7 +188,12 @@ function EditorCanvas( { } // Set default layout for classic themes so all alignments are supported. return { type: 'default' }; - }, [ renderingMode, themeSupportsLayout, globalLayoutSettings ] ); + }, [ + renderingMode, + themeSupportsLayout, + globalLayoutSettings, + isDesignPostType, + ] ); const newestPostContentAttributes = useMemo( () => { if ( @@ -318,7 +336,8 @@ function EditorCanvas( { > { themeSupportsLayout && ! themeHasDisabledLayoutStyles && - renderingMode === 'post-only' && ( + renderingMode === 'post-only' && + ! isDesignPostType && ( <> ) } - { renderingMode === 'post-only' && ( + { renderingMode === 'post-only' && ! isDesignPostType && (
Date: Tue, 13 Feb 2024 13:06:18 +0200 Subject: [PATCH 04/78] DataViews: Remove second `reset filter` button in filter dialog (#58960) Co-authored-by: ntsekouras Co-authored-by: jameskoster --- packages/dataviews/src/filter-summary.js | 39 ------------------------ packages/dataviews/src/style.scss | 5 --- 2 files changed, 44 deletions(-) diff --git a/packages/dataviews/src/filter-summary.js b/packages/dataviews/src/filter-summary.js index bbe0000c05dcbd..6e1e3e9e2620b1 100644 --- a/packages/dataviews/src/filter-summary.js +++ b/packages/dataviews/src/filter-summary.js @@ -8,7 +8,6 @@ import classnames from 'classnames'; */ import { Dropdown, - Button, __experimentalVStack as VStack, __experimentalHStack as HStack, FlexItem, @@ -133,40 +132,6 @@ function OperatorSelector( { filter, view, onChangeView } ) { ); } -function ResetFilter( { filter, view, onChangeView, addFilterRef } ) { - const isDisabled = - filter.isPrimary && - view.filters.find( ( _filter ) => _filter.field === filter.field ) - ?.value === undefined; - return ( -
- -
- ); -} - export default function FilterSummary( { addFilterRef, openedFilter, @@ -269,10 +234,6 @@ export default function FilterSummary( { - ); } } diff --git a/packages/dataviews/src/style.scss b/packages/dataviews/src/style.scss index afb5df37cc2dd2..7b143958e2c16e 100644 --- a/packages/dataviews/src/style.scss +++ b/packages/dataviews/src/style.scss @@ -627,11 +627,6 @@ } } -.dataviews-filter-summary__reset { - padding: $grid-unit-05; - border-top: 1px solid $gray-200; -} - .dataviews-filter-summary__chip-container { position: relative; white-space: pre-wrap; From 73cd992f9640205d4849b5815478d404c107e3a1 Mon Sep 17 00:00:00 2001 From: Nik Tsekouras Date: Tue, 13 Feb 2024 13:55:51 +0200 Subject: [PATCH 05/78] Remove old patterns list code and styles (#58966) Co-authored-by: ntsekouras Co-authored-by: youknowriad --- .../page-patterns/duplicate-menu-item.js | 105 ------ .../src/components/page-patterns/grid-item.js | 331 ------------------ .../src/components/page-patterns/grid.js | 22 -- .../src/components/page-patterns/index.js | 1 - .../components/page-patterns/no-patterns.js | 12 - .../components/page-patterns/patterns-list.js | 229 ------------ .../page-patterns/rename-menu-item.js | 132 ------- .../src/components/page-patterns/style.scss | 182 ---------- 8 files changed, 1014 deletions(-) delete mode 100644 packages/edit-site/src/components/page-patterns/duplicate-menu-item.js delete mode 100644 packages/edit-site/src/components/page-patterns/grid-item.js delete mode 100644 packages/edit-site/src/components/page-patterns/grid.js delete mode 100644 packages/edit-site/src/components/page-patterns/no-patterns.js delete mode 100644 packages/edit-site/src/components/page-patterns/patterns-list.js delete mode 100644 packages/edit-site/src/components/page-patterns/rename-menu-item.js diff --git a/packages/edit-site/src/components/page-patterns/duplicate-menu-item.js b/packages/edit-site/src/components/page-patterns/duplicate-menu-item.js deleted file mode 100644 index e82666902ed16a..00000000000000 --- a/packages/edit-site/src/components/page-patterns/duplicate-menu-item.js +++ /dev/null @@ -1,105 +0,0 @@ -/** - * WordPress dependencies - */ -import { MenuItem } from '@wordpress/components'; -import { useDispatch } from '@wordpress/data'; -import { useState } from '@wordpress/element'; -import { __, sprintf } from '@wordpress/i18n'; -import { store as noticesStore } from '@wordpress/notices'; -import { privateApis as patternsPrivateApis } from '@wordpress/patterns'; -import { privateApis as routerPrivateApis } from '@wordpress/router'; - -/** - * Internal dependencies - */ -import { TEMPLATE_PART_POST_TYPE, PATTERN_TYPES } from '../../utils/constants'; -import { unlock } from '../../lock-unlock'; -import CreateTemplatePartModal from '../create-template-part-modal'; - -const { DuplicatePatternModal } = unlock( patternsPrivateApis ); -const { useHistory } = unlock( routerPrivateApis ); - -export default function DuplicateMenuItem( { - categoryId, - item, - label = __( 'Duplicate' ), - onClose, -} ) { - const { createSuccessNotice } = useDispatch( noticesStore ); - const [ isModalOpen, setIsModalOpen ] = useState( false ); - const history = useHistory(); - - const closeModal = () => setIsModalOpen( false ); - - const isTemplatePart = item.type === TEMPLATE_PART_POST_TYPE; - const isThemePattern = item.type === PATTERN_TYPES.theme; - - async function onTemplatePartSuccess( templatePart ) { - createSuccessNotice( - sprintf( - // translators: %s: The new template part's title e.g. 'Call to action (copy)'. - __( '"%s" duplicated.' ), - item.title - ), - { - type: 'snackbar', - id: 'edit-site-patterns-success', - } - ); - - history.push( { - postType: TEMPLATE_PART_POST_TYPE, - postId: templatePart?.id, - categoryType: TEMPLATE_PART_POST_TYPE, - categoryId, - } ); - - onClose(); - } - - function onPatternSuccess( { pattern } ) { - history.push( { - categoryType: PATTERN_TYPES.theme, - categoryId, - postType: PATTERN_TYPES.user, - postId: pattern.id, - } ); - - onClose(); - } - - return ( - <> - setIsModalOpen( true ) } - aria-expanded={ isModalOpen } - aria-haspopup="dialog" - > - { label } - - { isModalOpen && ! isTemplatePart && ( - - ) } - { isModalOpen && isTemplatePart && ( - - ) } - - ); -} diff --git a/packages/edit-site/src/components/page-patterns/grid-item.js b/packages/edit-site/src/components/page-patterns/grid-item.js deleted file mode 100644 index 0c1b162dac99d4..00000000000000 --- a/packages/edit-site/src/components/page-patterns/grid-item.js +++ /dev/null @@ -1,331 +0,0 @@ -/** - * External dependencies - */ -import classnames from 'classnames'; -import { paramCase as kebabCase } from 'change-case'; - -/** - * WordPress dependencies - */ -import { - BlockPreview, - privateApis as blockEditorPrivateApis, -} from '@wordpress/block-editor'; -import { - Button, - __experimentalConfirmDialog as ConfirmDialog, - DropdownMenu, - MenuGroup, - MenuItem, - __experimentalHeading as Heading, - __experimentalHStack as HStack, - Tooltip, - Flex, -} from '@wordpress/components'; -import { useDispatch } from '@wordpress/data'; -import { useState, useId, memo } from '@wordpress/element'; -import { __, sprintf } from '@wordpress/i18n'; -import { - Icon, - header, - footer, - symbolFilled as uncategorized, - symbol, - moreVertical, - lockSmall, -} from '@wordpress/icons'; -import { store as noticesStore } from '@wordpress/notices'; -import { store as reusableBlocksStore } from '@wordpress/reusable-blocks'; -import { downloadBlob } from '@wordpress/blob'; - -/** - * Internal dependencies - */ -import RenameMenuItem from './rename-menu-item'; -import DuplicateMenuItem from './duplicate-menu-item'; -import { - PATTERN_TYPES, - TEMPLATE_PART_POST_TYPE, - PATTERN_SYNC_TYPES, -} from '../../utils/constants'; -import { store as editSiteStore } from '../../store'; -import { useLink } from '../routes/link'; -import { unlock } from '../../lock-unlock'; - -const { useGlobalStyle } = unlock( blockEditorPrivateApis ); - -const templatePartIcons = { header, footer, uncategorized }; - -function GridItem( { categoryId, item, ...props } ) { - const descriptionId = useId(); - const [ isDeleteDialogOpen, setIsDeleteDialogOpen ] = useState( false ); - const [ backgroundColor ] = useGlobalStyle( 'color.background' ); - - const { removeTemplate } = useDispatch( editSiteStore ); - const { __experimentalDeleteReusableBlock } = - useDispatch( reusableBlocksStore ); - const { createErrorNotice, createSuccessNotice } = - useDispatch( noticesStore ); - - const isUserPattern = item.type === PATTERN_TYPES.user; - const isNonUserPattern = item.type === PATTERN_TYPES.theme; - const isTemplatePart = item.type === TEMPLATE_PART_POST_TYPE; - - const { onClick } = useLink( { - postType: item.type, - postId: isUserPattern ? item.id : item.name, - categoryId, - categoryType: isTemplatePart ? item.type : PATTERN_TYPES.theme, - } ); - - const isEmpty = ! item.blocks?.length; - const patternClassNames = classnames( 'edit-site-patterns__pattern', { - 'is-placeholder': isEmpty, - } ); - const previewClassNames = classnames( 'edit-site-patterns__preview', { - 'is-inactive': isNonUserPattern, - } ); - - const deletePattern = async () => { - try { - await __experimentalDeleteReusableBlock( item.id ); - createSuccessNotice( - sprintf( - // translators: %s: The pattern's title e.g. 'Call to action'. - __( '"%s" deleted.' ), - item.title - ), - { type: 'snackbar', id: 'edit-site-patterns-success' } - ); - } catch ( error ) { - const errorMessage = - error.message && error.code !== 'unknown_error' - ? error.message - : __( 'An error occurred while deleting the pattern.' ); - createErrorNotice( errorMessage, { - type: 'snackbar', - id: 'edit-site-patterns-error', - } ); - } - }; - const deleteItem = () => - isTemplatePart ? removeTemplate( item ) : deletePattern(); - const exportAsJSON = () => { - const json = { - __file: item.type, - title: item.title || item.name, - content: item.patternPost.content.raw, - syncStatus: item.patternPost.wp_pattern_sync_status, - }; - - return downloadBlob( - `${ kebabCase( item.title || item.name ) }.json`, - JSON.stringify( json, null, 2 ), - 'application/json' - ); - }; - - // Only custom patterns or custom template parts can be renamed or deleted. - const isCustomPattern = - isUserPattern || ( isTemplatePart && item.isCustom ); - const hasThemeFile = isTemplatePart && item.templatePart.has_theme_file; - const ariaDescriptions = []; - - if ( isCustomPattern ) { - // User patterns don't have descriptions, but can be edited and deleted, so include some help text. - ariaDescriptions.push( - __( 'Press Enter to edit, or Delete to delete the pattern.' ) - ); - } else if ( item.description ) { - ariaDescriptions.push( item.description ); - } - - if ( isNonUserPattern ) { - ariaDescriptions.push( - __( 'Theme & plugin patterns cannot be edited.' ) - ); - } - - let itemIcon; - if ( ! isUserPattern && templatePartIcons[ categoryId ] ) { - itemIcon = templatePartIcons[ categoryId ]; - } else { - itemIcon = - item.syncStatus === PATTERN_SYNC_TYPES.full ? symbol : undefined; - } - - const confirmButtonText = hasThemeFile ? __( 'Clear' ) : __( 'Delete' ); - const confirmPrompt = hasThemeFile - ? __( 'Are you sure you want to clear these customizations?' ) - : sprintf( - // translators: %s: The pattern or template part's title e.g. 'Call to action'. - __( 'Are you sure you want to delete "%s"?' ), - item.title || item.name - ); - - const additionalStyles = ! backgroundColor - ? [ { css: 'body { background: #fff; }' } ] - : undefined; - - return ( -
  • - - { ariaDescriptions.map( ( ariaDescription, index ) => ( - - ) ) } - - - { itemIcon && ! isNonUserPattern && ( - - - - ) } - - { item.type === PATTERN_TYPES.theme ? ( - item.title - ) : ( - - - - ) } - { item.type === PATTERN_TYPES.theme && ( - - - - ) } - - - - { ( { onClose } ) => ( - - { isCustomPattern && ! hasThemeFile && ( - - ) } - - { item.type === PATTERN_TYPES.user && ( - exportAsJSON() }> - { __( 'Export as JSON' ) } - - ) } - - { isCustomPattern && ( - - setIsDeleteDialogOpen( true ) - } - > - { hasThemeFile - ? __( 'Clear customizations' ) - : __( 'Delete' ) } - - ) } - - ) } - - - - { isDeleteDialogOpen && ( - setIsDeleteDialogOpen( false ) } - > - { confirmPrompt } - - ) } -
  • - ); -} - -export default memo( GridItem ); diff --git a/packages/edit-site/src/components/page-patterns/grid.js b/packages/edit-site/src/components/page-patterns/grid.js deleted file mode 100644 index 59a8cc431567f9..00000000000000 --- a/packages/edit-site/src/components/page-patterns/grid.js +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Internal dependencies - */ -import GridItem from './grid-item'; - -export default function Grid( { categoryId, items, ...props } ) { - if ( ! items?.length ) { - return null; - } - - return ( -
      - { items.map( ( item ) => ( - - ) ) } -
    - ); -} diff --git a/packages/edit-site/src/components/page-patterns/index.js b/packages/edit-site/src/components/page-patterns/index.js index 5564ae05146184..9cf02dbd3e2ab4 100644 --- a/packages/edit-site/src/components/page-patterns/index.js +++ b/packages/edit-site/src/components/page-patterns/index.js @@ -391,7 +391,6 @@ export default function DataviewsPatterns() { // Wrap everything in a block editor provider. // This ensures 'styles' that are needed for the previews are synced // from the site editor store to the block editor store. - // TODO: check if I add the provider in every preview like in templates... return ( - { __( 'No patterns found.' ) } -
    - ); -} diff --git a/packages/edit-site/src/components/page-patterns/patterns-list.js b/packages/edit-site/src/components/page-patterns/patterns-list.js deleted file mode 100644 index 91ca083607674d..00000000000000 --- a/packages/edit-site/src/components/page-patterns/patterns-list.js +++ /dev/null @@ -1,229 +0,0 @@ -/** - * WordPress dependencies - */ -import { useState, useDeferredValue, useId, useMemo } from '@wordpress/element'; -import { - SearchControl, - __experimentalVStack as VStack, - Flex, - FlexBlock, - __experimentalToggleGroupControl as ToggleGroupControl, - __experimentalToggleGroupControlOption as ToggleGroupControlOption, - __experimentalHeading as Heading, - __experimentalText as Text, -} from '@wordpress/components'; -import { __, _x, isRTL } from '@wordpress/i18n'; -import { chevronLeft, chevronRight } from '@wordpress/icons'; -import { privateApis as routerPrivateApis } from '@wordpress/router'; -import { - useAsyncList, - useViewportMatch, - useDebouncedInput, -} from '@wordpress/compose'; - -/** - * Internal dependencies - */ -import PatternsHeader from './header'; -import Grid from './grid'; -import NoPatterns from './no-patterns'; -import usePatterns from './use-patterns'; -import SidebarButton from '../sidebar-button'; -import { unlock } from '../../lock-unlock'; -import { PATTERN_SYNC_TYPES, PATTERN_TYPES } from '../../utils/constants'; -import Pagination from '../pagination'; - -const { useLocation, useHistory } = unlock( routerPrivateApis ); - -const SYNC_FILTERS = { - all: _x( 'All', 'Option that shows all patterns' ), - [ PATTERN_SYNC_TYPES.full ]: _x( - 'Synced', - 'Option that shows all synchronized patterns' - ), - [ PATTERN_SYNC_TYPES.unsynced ]: _x( - 'Not synced', - 'Option that shows all patterns that are not synchronized' - ), -}; - -const SYNC_DESCRIPTIONS = { - all: '', - [ PATTERN_SYNC_TYPES.full ]: __( - 'Patterns that are kept in sync across the site.' - ), - [ PATTERN_SYNC_TYPES.unsynced ]: __( - 'Patterns that can be changed freely without affecting the site.' - ), -}; - -const PAGE_SIZE = 20; - -export default function PatternsList( { categoryId, type } ) { - const location = useLocation(); - const history = useHistory(); - const isMobileViewport = useViewportMatch( 'medium', '<' ); - const [ filterValue, setFilterValue, delayedFilterValue ] = - useDebouncedInput( '' ); - const deferredFilterValue = useDeferredValue( delayedFilterValue ); - - const [ syncFilter, setSyncFilter ] = useState( 'all' ); - const [ currentPage, setCurrentPage ] = useState( 1 ); - - const deferredSyncedFilter = useDeferredValue( syncFilter ); - - const isUncategorizedThemePatterns = - type === PATTERN_TYPES.theme && categoryId === 'uncategorized'; - - const { patterns, isResolving } = usePatterns( - type, - isUncategorizedThemePatterns ? '' : categoryId, - { - search: deferredFilterValue, - syncStatus: - deferredSyncedFilter === 'all' - ? undefined - : deferredSyncedFilter, - } - ); - - const updateSearchFilter = ( value ) => { - setCurrentPage( 1 ); - setFilterValue( value ); - }; - - const updateSyncFilter = ( value ) => { - setCurrentPage( 1 ); - setSyncFilter( value ); - }; - - const id = useId(); - const titleId = `${ id }-title`; - const descriptionId = `${ id }-description`; - - const hasPatterns = patterns.length; - const title = SYNC_FILTERS[ syncFilter ]; - const description = SYNC_DESCRIPTIONS[ syncFilter ]; - - const totalItems = patterns.length; - const pageIndex = currentPage - 1; - const numPages = Math.ceil( patterns.length / PAGE_SIZE ); - - const list = useMemo( () => { - return patterns.slice( - pageIndex * PAGE_SIZE, - pageIndex * PAGE_SIZE + PAGE_SIZE - ); - }, [ pageIndex, patterns ] ); - - const asyncList = useAsyncList( list, { step: 10 } ); - - const changePage = ( page ) => { - const scrollContainer = document.querySelector( '.edit-site-patterns' ); - scrollContainer?.scrollTo( 0, 0 ); - - setCurrentPage( page ); - }; - - return ( - <> - - - - { isMobileViewport && ( - { - // Go back in history if we came from the Patterns page. - // Otherwise push a stack onto the history. - if ( - location.state?.backPath === '/patterns' - ) { - history.back(); - } else { - history.push( { path: '/patterns' } ); - } - } } - /> - ) } - - - updateSearchFilter( value ) - } - placeholder={ __( 'Search patterns' ) } - label={ __( 'Search patterns' ) } - value={ filterValue } - __nextHasNoMarginBottom - /> - - { type === PATTERN_TYPES.theme && ( - updateSyncFilter( value ) } - __nextHasNoMarginBottom - > - { Object.entries( SYNC_FILTERS ).map( - ( [ key, label ] ) => ( - - ) - ) } - - ) } - - - - { syncFilter !== 'all' && ( - - - { title } - - { description ? ( - - { description } - - ) : null } - - ) } - { hasPatterns && ( - - ) } - { ! isResolving && ! hasPatterns && } - - { numPages > 1 && ( - - ) } - - ); -} diff --git a/packages/edit-site/src/components/page-patterns/rename-menu-item.js b/packages/edit-site/src/components/page-patterns/rename-menu-item.js deleted file mode 100644 index c2b3b960fb6677..00000000000000 --- a/packages/edit-site/src/components/page-patterns/rename-menu-item.js +++ /dev/null @@ -1,132 +0,0 @@ -/** - * WordPress dependencies - */ -import { - Button, - MenuItem, - Modal, - TextControl, - __experimentalHStack as HStack, - __experimentalVStack as VStack, -} from '@wordpress/components'; -import { store as coreStore } from '@wordpress/core-data'; -import { useDispatch } from '@wordpress/data'; -import { useState } from '@wordpress/element'; -import { __ } from '@wordpress/i18n'; -import { store as noticesStore } from '@wordpress/notices'; - -/** - * Internal dependencies - */ -import { TEMPLATE_PART_POST_TYPE } from '../../utils/constants'; - -export default function RenameMenuItem( { item, onClose } ) { - const [ title, setTitle ] = useState( () => item.title ); - const [ isModalOpen, setIsModalOpen ] = useState( false ); - - const { editEntityRecord, saveEditedEntityRecord } = - useDispatch( coreStore ); - const { createSuccessNotice, createErrorNotice } = - useDispatch( noticesStore ); - - if ( item.type === TEMPLATE_PART_POST_TYPE && ! item.isCustom ) { - return null; - } - - async function onRename( event ) { - event.preventDefault(); - - try { - await editEntityRecord( 'postType', item.type, item.id, { title } ); - - // Update state before saving rerenders the list. - setTitle( '' ); - setIsModalOpen( false ); - onClose(); - - // Persist edited entity. - await saveEditedEntityRecord( 'postType', item.type, item.id, { - throwOnError: true, - } ); - - createSuccessNotice( - item.type === TEMPLATE_PART_POST_TYPE - ? __( 'Template part renamed.' ) - : __( 'Pattern renamed.' ), - { - type: 'snackbar', - } - ); - } catch ( error ) { - const fallbackErrorMessage = - item.type === TEMPLATE_PART_POST_TYPE - ? __( - 'An error occurred while renaming the template part.' - ) - : __( 'An error occurred while renaming the pattern.' ); - const errorMessage = - error.message && error.code !== 'unknown_error' - ? error.message - : fallbackErrorMessage; - - createErrorNotice( errorMessage, { type: 'snackbar' } ); - } - } - - return ( - <> - { - setIsModalOpen( true ); - setTitle( item.title ); - } } - > - { __( 'Rename' ) } - - { isModalOpen && ( - { - setIsModalOpen( false ); - onClose(); - } } - overlayClassName="edit-site-list__rename-modal" - > -
    - - - - - - - - - -
    -
    - ) } - - ); -} diff --git a/packages/edit-site/src/components/page-patterns/style.scss b/packages/edit-site/src/components/page-patterns/style.scss index e5bd44956b2629..89f38e7d4c9a63 100644 --- a/packages/edit-site/src/components/page-patterns/style.scss +++ b/packages/edit-site/src/components/page-patterns/style.scss @@ -1,70 +1,3 @@ -.edit-site-patterns { - background: #1e1e1e; - border-left: 1px solid $gray-800; - margin: $header-height 0 0; - border-radius: 0; - padding: 0; - overflow-x: auto; - min-height: 100%; - - .components-base-control { - width: 100%; - @include break-medium { - width: auto; - } - } - - .components-text { - color: $gray-600; - } - - .components-heading { - color: $gray-200; - } - - @include break-medium { - margin: 0; - } - - .edit-site-patterns__search-block { - min-width: fit-content; - flex-grow: 1; - } - - // TODO: Consider using the Theme component to automatically adapt to a dark background. - .edit-site-patterns__search { - --wp-components-color-foreground: #{$gray-200}; - - .components-input-control__container { - background: $gray-800; - } - - svg { - fill: $gray-600; - } - } - - .edit-site-patterns__sync-status-filter { - background: $gray-800; - border: none; - height: $button-size-next-default-40px; - min-width: max-content; - width: 100%; - max-width: 100%; - - @include break-medium { - width: 300px; - } - } - .edit-site-patterns__sync-status-filter-option:not([aria-checked="true"]) { - color: $gray-600; - } - .edit-site-patterns__sync-status-filter-option:active { - background: $gray-700; - color: $gray-100; - } -} - .edit-site-patterns__header { position: sticky; top: 0; @@ -77,97 +10,13 @@ } } -.edit-site-patterns__section { - padding: $grid-unit-30 $grid-unit-40; - flex: 1; -} - .edit-site-patterns__section-header { .screen-reader-shortcut:focus { top: 0; } } -.edit-site-patterns__grid { - display: grid; - grid-template-columns: 1fr; - gap: $grid-unit-40; - margin-top: 0; - margin-bottom: 0; - @include break-large { - grid-template-columns: 1fr 1fr; - } - @include break-huge { - grid-template-columns: 1fr 1fr 1fr; - } - @include break-xhuge { - grid-template-columns: 1fr 1fr 1fr 1fr; - } - .edit-site-patterns__pattern { - break-inside: avoid-column; - display: flex; - flex-direction: column; - .edit-site-patterns__preview { - box-shadow: none; - border: none; - padding: 0; - background-color: unset; - box-sizing: border-box; - border-radius: 4px; - cursor: pointer; - overflow: hidden; - - &:focus { - box-shadow: inset 0 0 0 0 $white, 0 0 0 2px var(--wp-admin-theme-color); - // Windows High Contrast mode will show this outline, but not the box-shadow. - outline: 2px solid transparent; - } - - &.is-inactive { - cursor: default; - } - &.is-inactive:focus { - box-shadow: 0 0 0 var(--wp-admin-border-width-focus) $gray-800; - opacity: 0.8; - } - } - - .edit-site-patterns__footer, - .edit-site-patterns__button { - color: $gray-600; - } - - .edit-site-patterns__dropdown { - flex-shrink: 0; - } - - &.is-placeholder .edit-site-patterns__preview { - min-height: $grid-unit-80; - color: $gray-600; - border: 1px dashed $gray-800; - display: flex; - align-items: center; - justify-content: center; - - &:focus { - box-shadow: 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); - } - } - } - - .edit-site-patterns__preview { - flex: 0 1 auto; - margin-bottom: $grid-unit-15; - } -} - -.edit-site-patterns__load-more { - align-self: center; -} - .edit-site-patterns__pattern-title { - color: $gray-200; - .is-link { text-decoration: none; color: $gray-200; @@ -189,41 +38,10 @@ } } -.edit-site-patterns__no-results { - color: $gray-600; -} - .edit-site-patterns__delete-modal { width: $modal-width-small; } -.edit-site-patterns__pagination { - padding: $grid-unit-30 $grid-unit-40; - border-top: 1px solid $gray-800; - background: $gray-900; - position: sticky; - bottom: 0; - color: $gray-100; - z-index: z-index(".edit-site-patterns__grid-pagination"); - .components-button.is-tertiary { - background-color: $gray-800; - color: $gray-100; - - &:disabled { - color: $gray-600; - background: none; - } - - &:hover:not(:disabled) { - background-color: $gray-700; - } - } -} - -/** - * DataViews patterns styles. - * TODO: when this becomes stable, consolidate styles with the above. - */ .edit-site-page-patterns-dataviews { .page-patterns-preview-field { display: flex; From 8708ca834764b3997e3041b88dbaaba27e6190bf Mon Sep 17 00:00:00 2001 From: Gutenberg Repository Automation Date: Tue, 13 Feb 2024 12:21:38 +0000 Subject: [PATCH 06/78] Update changelog files --- packages/components/CHANGELOG.md | 2 ++ packages/components/package.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 3a749a22c536ae..b6d1af65f61f72 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 26.0.1 (2024-02-13) + ### Bug Fix - `Modal`: Add `box-sizing` reset style ([#58905](https://github.com/WordPress/gutenberg/pull/58905)). diff --git a/packages/components/package.json b/packages/components/package.json index 3b3e9e609ce58c..a04284dd9a067b 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/components", - "version": "26.0.0", + "version": "26.0.1-prerelease", "description": "UI components for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", From 5fd4953ada078e923caa99f79553ca7412fec6e4 Mon Sep 17 00:00:00 2001 From: Gutenberg Repository Automation Date: Tue, 13 Feb 2024 12:23:25 +0000 Subject: [PATCH 07/78] chore(release): publish - @wordpress/annotations@2.51.1 - @wordpress/block-directory@4.28.1 - @wordpress/block-editor@12.19.1 - @wordpress/block-library@8.28.1 - @wordpress/blocks@12.28.1 - @wordpress/commands@0.22.1 - @wordpress/components@26.0.1 - @wordpress/core-commands@0.20.1 - @wordpress/core-data@6.28.1 - @wordpress/customize-widgets@4.28.1 - @wordpress/dataviews@0.5.1 - @wordpress/edit-post@7.28.1 - @wordpress/edit-site@5.28.1 - @wordpress/edit-widgets@5.28.1 - @wordpress/editor@13.28.1 - @wordpress/format-library@4.28.1 - @wordpress/interface@5.28.1 - @wordpress/list-reusable-blocks@4.28.1 - @wordpress/nux@8.13.1 - @wordpress/patterns@1.12.1 - @wordpress/plugins@6.19.1 - @wordpress/preferences@3.28.1 - @wordpress/reusable-blocks@4.28.1 - @wordpress/rich-text@6.28.1 - @wordpress/server-side-render@4.28.1 - @wordpress/widgets@3.28.1 --- package-lock.json | 52 +++++++++++----------- packages/annotations/package.json | 2 +- packages/block-directory/package.json | 2 +- packages/block-editor/package.json | 2 +- packages/block-library/package.json | 2 +- packages/blocks/package.json | 2 +- packages/commands/package.json | 2 +- packages/components/package.json | 2 +- packages/core-commands/package.json | 2 +- packages/core-data/package.json | 2 +- packages/customize-widgets/package.json | 2 +- packages/dataviews/package.json | 2 +- packages/edit-post/package.json | 2 +- packages/edit-site/package.json | 2 +- packages/edit-widgets/package.json | 2 +- packages/editor/package.json | 2 +- packages/format-library/package.json | 2 +- packages/interface/package.json | 2 +- packages/list-reusable-blocks/package.json | 2 +- packages/nux/package.json | 2 +- packages/patterns/package.json | 2 +- packages/plugins/package.json | 2 +- packages/preferences/package.json | 2 +- packages/reusable-blocks/package.json | 2 +- packages/rich-text/package.json | 2 +- packages/server-side-render/package.json | 2 +- packages/widgets/package.json | 2 +- 27 files changed, 52 insertions(+), 52 deletions(-) diff --git a/package-lock.json b/package-lock.json index f3885e822e3bd8..15002da92150ff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -53554,7 +53554,7 @@ }, "packages/annotations": { "name": "@wordpress/annotations", - "version": "2.51.0", + "version": "2.51.1", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -53675,7 +53675,7 @@ }, "packages/block-directory": { "name": "@wordpress/block-directory", - "version": "4.28.0", + "version": "4.28.1", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -53710,7 +53710,7 @@ }, "packages/block-editor": { "name": "@wordpress/block-editor", - "version": "12.19.0", + "version": "12.19.1", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -53807,7 +53807,7 @@ }, "packages/block-library": { "name": "@wordpress/block-library", - "version": "8.28.0", + "version": "8.28.1", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -53894,7 +53894,7 @@ }, "packages/blocks": { "name": "@wordpress/blocks", - "version": "12.28.0", + "version": "12.28.1", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -53957,7 +53957,7 @@ }, "packages/commands": { "name": "@wordpress/commands", - "version": "0.22.0", + "version": "0.22.1", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -53982,7 +53982,7 @@ }, "packages/components": { "name": "@wordpress/components", - "version": "26.0.0", + "version": "26.0.1", "license": "GPL-2.0-or-later", "dependencies": { "@ariakit/react": "^0.3.12", @@ -54110,7 +54110,7 @@ }, "packages/core-commands": { "name": "@wordpress/core-commands", - "version": "0.20.0", + "version": "0.20.1", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54136,7 +54136,7 @@ }, "packages/core-data": { "name": "@wordpress/core-data", - "version": "6.28.0", + "version": "6.28.1", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54214,7 +54214,7 @@ }, "packages/customize-widgets": { "name": "@wordpress/customize-widgets", - "version": "4.28.0", + "version": "4.28.1", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54296,7 +54296,7 @@ }, "packages/dataviews": { "name": "@wordpress/dataviews", - "version": "0.5.0", + "version": "0.5.1", "license": "GPL-2.0-or-later", "dependencies": { "@ariakit/react": "^0.3.12", @@ -54523,7 +54523,7 @@ }, "packages/edit-post": { "name": "@wordpress/edit-post", - "version": "7.28.0", + "version": "7.28.1", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54571,7 +54571,7 @@ }, "packages/edit-site": { "name": "@wordpress/edit-site", - "version": "5.28.0", + "version": "5.28.1", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54636,7 +54636,7 @@ }, "packages/edit-widgets": { "name": "@wordpress/edit-widgets", - "version": "5.28.0", + "version": "5.28.1", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54679,7 +54679,7 @@ }, "packages/editor": { "name": "@wordpress/editor", - "version": "13.28.0", + "version": "13.28.1", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54936,7 +54936,7 @@ }, "packages/format-library": { "name": "@wordpress/format-library", - "version": "4.28.0", + "version": "4.28.1", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55090,7 +55090,7 @@ }, "packages/interface": { "name": "@wordpress/interface", - "version": "5.28.0", + "version": "5.28.1", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55228,7 +55228,7 @@ }, "packages/list-reusable-blocks": { "name": "@wordpress/list-reusable-blocks", - "version": "4.28.0", + "version": "4.28.1", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55293,7 +55293,7 @@ }, "packages/nux": { "name": "@wordpress/nux", - "version": "8.13.0", + "version": "8.13.1", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55316,7 +55316,7 @@ }, "packages/patterns": { "name": "@wordpress/patterns", - "version": "1.12.0", + "version": "1.12.1", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55346,7 +55346,7 @@ }, "packages/plugins": { "name": "@wordpress/plugins", - "version": "6.19.0", + "version": "6.19.1", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55396,7 +55396,7 @@ }, "packages/preferences": { "name": "@wordpress/preferences", - "version": "3.28.0", + "version": "3.28.1", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55698,7 +55698,7 @@ }, "packages/reusable-blocks": { "name": "@wordpress/reusable-blocks", - "version": "4.28.0", + "version": "4.28.1", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55724,7 +55724,7 @@ }, "packages/rich-text": { "name": "@wordpress/rich-text", - "version": "6.28.0", + "version": "6.28.1", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55988,7 +55988,7 @@ }, "packages/server-side-render": { "name": "@wordpress/server-side-render", - "version": "4.28.0", + "version": "4.28.1", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -56133,7 +56133,7 @@ }, "packages/widgets": { "name": "@wordpress/widgets", - "version": "3.28.0", + "version": "3.28.1", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", diff --git a/packages/annotations/package.json b/packages/annotations/package.json index edbc730b35c68e..1257778703aa18 100644 --- a/packages/annotations/package.json +++ b/packages/annotations/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/annotations", - "version": "2.51.0", + "version": "2.51.1", "description": "Annotate content in the Gutenberg editor.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/block-directory/package.json b/packages/block-directory/package.json index ab1a4ebeb8a021..d892e359511938 100644 --- a/packages/block-directory/package.json +++ b/packages/block-directory/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/block-directory", - "version": "4.28.0", + "version": "4.28.1", "description": "Extend editor with block directory features to search, download and install blocks.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/block-editor/package.json b/packages/block-editor/package.json index 2438e6db4a9594..59eede6f9d4db0 100644 --- a/packages/block-editor/package.json +++ b/packages/block-editor/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/block-editor", - "version": "12.19.0", + "version": "12.19.1", "description": "Generic block editor.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/block-library/package.json b/packages/block-library/package.json index 90f17f96f69a89..b27704fcd52fb7 100644 --- a/packages/block-library/package.json +++ b/packages/block-library/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/block-library", - "version": "8.28.0", + "version": "8.28.1", "description": "Block library for the WordPress editor.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/blocks/package.json b/packages/blocks/package.json index 26646516505437..aca9b8bf061c6b 100644 --- a/packages/blocks/package.json +++ b/packages/blocks/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/blocks", - "version": "12.28.0", + "version": "12.28.1", "description": "Block API for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/commands/package.json b/packages/commands/package.json index b2f954aacbb297..3f259f5af2570e 100644 --- a/packages/commands/package.json +++ b/packages/commands/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/commands", - "version": "0.22.0", + "version": "0.22.1", "description": "Handles the commands menu.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/components/package.json b/packages/components/package.json index a04284dd9a067b..32c3620ec45190 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/components", - "version": "26.0.1-prerelease", + "version": "26.0.1", "description": "UI components for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/core-commands/package.json b/packages/core-commands/package.json index ce24d857f1e7f5..afaec5b550dc96 100644 --- a/packages/core-commands/package.json +++ b/packages/core-commands/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/core-commands", - "version": "0.20.0", + "version": "0.20.1", "description": "WordPress core reusable commands.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/core-data/package.json b/packages/core-data/package.json index 969d00e18d782d..89cc98a22e2f48 100644 --- a/packages/core-data/package.json +++ b/packages/core-data/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/core-data", - "version": "6.28.0", + "version": "6.28.1", "description": "Access to and manipulation of core WordPress entities.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/customize-widgets/package.json b/packages/customize-widgets/package.json index ce612b0a96fdf4..c0c07964f88abd 100644 --- a/packages/customize-widgets/package.json +++ b/packages/customize-widgets/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/customize-widgets", - "version": "4.28.0", + "version": "4.28.1", "description": "Widgets blocks in Customizer Module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/dataviews/package.json b/packages/dataviews/package.json index 17c15108550751..d1a0f5285634b4 100644 --- a/packages/dataviews/package.json +++ b/packages/dataviews/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/dataviews", - "version": "0.5.0", + "version": "0.5.1", "description": "DataViews is a component that provides an API to render datasets using different types of layouts (table, grid, list, etc.).", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/edit-post/package.json b/packages/edit-post/package.json index 525bc0280da9ab..a4a7a210418e43 100644 --- a/packages/edit-post/package.json +++ b/packages/edit-post/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/edit-post", - "version": "7.28.0", + "version": "7.28.1", "description": "Edit Post module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/edit-site/package.json b/packages/edit-site/package.json index a7228e5caf955b..c4f31d385e5d2e 100644 --- a/packages/edit-site/package.json +++ b/packages/edit-site/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/edit-site", - "version": "5.28.0", + "version": "5.28.1", "description": "Edit Site Page module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/edit-widgets/package.json b/packages/edit-widgets/package.json index 63777a0706d295..dacfe0f79d1bdc 100644 --- a/packages/edit-widgets/package.json +++ b/packages/edit-widgets/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/edit-widgets", - "version": "5.28.0", + "version": "5.28.1", "description": "Widgets Page module for WordPress..", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/editor/package.json b/packages/editor/package.json index 9000d214d2276e..11c8b600b21959 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/editor", - "version": "13.28.0", + "version": "13.28.1", "description": "Enhanced block editor for WordPress posts.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/format-library/package.json b/packages/format-library/package.json index 08a0bb533497de..1746144bf327bb 100644 --- a/packages/format-library/package.json +++ b/packages/format-library/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/format-library", - "version": "4.28.0", + "version": "4.28.1", "description": "Format library for the WordPress editor.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/interface/package.json b/packages/interface/package.json index 316ca6b4a315d5..e34b8733d46e2a 100644 --- a/packages/interface/package.json +++ b/packages/interface/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/interface", - "version": "5.28.0", + "version": "5.28.1", "description": "Interface module for WordPress. The package contains shared functionality across the modern JavaScript-based WordPress screens.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/list-reusable-blocks/package.json b/packages/list-reusable-blocks/package.json index 98f3b64ef3f182..e794f9ddd1293e 100644 --- a/packages/list-reusable-blocks/package.json +++ b/packages/list-reusable-blocks/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/list-reusable-blocks", - "version": "4.28.0", + "version": "4.28.1", "description": "Adding Export/Import support to the reusable blocks listing.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/nux/package.json b/packages/nux/package.json index 0803094f025a99..e294dbde824495 100644 --- a/packages/nux/package.json +++ b/packages/nux/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/nux", - "version": "8.13.0", + "version": "8.13.1", "description": "NUX (New User eXperience) module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/patterns/package.json b/packages/patterns/package.json index e03e7bbd4e23da..143ed6e319ff5f 100644 --- a/packages/patterns/package.json +++ b/packages/patterns/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/patterns", - "version": "1.12.0", + "version": "1.12.1", "description": "Management of user pattern editing.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/plugins/package.json b/packages/plugins/package.json index 9933f5c11ad267..3e54c1d07c8c61 100644 --- a/packages/plugins/package.json +++ b/packages/plugins/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/plugins", - "version": "6.19.0", + "version": "6.19.1", "description": "Plugins module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/preferences/package.json b/packages/preferences/package.json index 1a696bd1392942..5aab74a778c938 100644 --- a/packages/preferences/package.json +++ b/packages/preferences/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/preferences", - "version": "3.28.0", + "version": "3.28.1", "description": "Utilities for managing WordPress preferences.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/reusable-blocks/package.json b/packages/reusable-blocks/package.json index 9b4e6188c29de1..c5368ac2de233d 100644 --- a/packages/reusable-blocks/package.json +++ b/packages/reusable-blocks/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/reusable-blocks", - "version": "4.28.0", + "version": "4.28.1", "description": "Reusable blocks utilities.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/rich-text/package.json b/packages/rich-text/package.json index f4da30d2988e78..5041594c3341b2 100644 --- a/packages/rich-text/package.json +++ b/packages/rich-text/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/rich-text", - "version": "6.28.0", + "version": "6.28.1", "description": "Rich text value and manipulation API.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/server-side-render/package.json b/packages/server-side-render/package.json index 342bce62ec7df4..a97d20d9bac4ed 100644 --- a/packages/server-side-render/package.json +++ b/packages/server-side-render/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/server-side-render", - "version": "4.28.0", + "version": "4.28.1", "description": "The component used with WordPress to server-side render a preview of dynamic blocks to display in the editor.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/widgets/package.json b/packages/widgets/package.json index e67f35a9bd267a..36a6b20a799879 100644 --- a/packages/widgets/package.json +++ b/packages/widgets/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/widgets", - "version": "3.28.0", + "version": "3.28.1", "description": "Functionality used by the widgets block editor in the Widgets screen and the Customizer.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", From 02dc8864c632d0127072993cb10006d6c002d84b Mon Sep 17 00:00:00 2001 From: Jason Crist Date: Tue, 13 Feb 2024 08:38:09 -0500 Subject: [PATCH 08/78] Font Library: Replace infinite scroll by pagination (#58794) * Eliminate and render fonts with pagination instead of as a large list Co-authored-by: pbking Co-authored-by: carolinan Co-authored-by: matiasbenedetto Co-authored-by: t-hamano Co-authored-by: afercia Co-authored-by: colorful-tones Co-authored-by: jasmussen Co-authored-by: okmttdhr --- .../collection-font-details.js | 2 +- .../font-library-modal/font-collection.js | 131 ++++++++++++++++-- .../font-library-modal/fonts-grid.js | 59 -------- .../font-library-modal/installed-fonts.js | 48 ++++--- .../font-library-modal/style.scss | 7 +- 5 files changed, 146 insertions(+), 101 deletions(-) delete mode 100644 packages/edit-site/src/components/global-styles/font-library-modal/fonts-grid.js diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/collection-font-details.js b/packages/edit-site/src/components/global-styles/font-library-modal/collection-font-details.js index de0c1cfa16ec6e..a6962952661939 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/collection-font-details.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/collection-font-details.js @@ -48,7 +48,7 @@ function CollectionFontDetails( { /> ) ) } - + ); } diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/font-collection.js b/packages/edit-site/src/components/global-styles/font-library-modal/font-collection.js index 4a14ee245694b8..6236ea8fe3f246 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/font-collection.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/font-collection.js @@ -1,11 +1,18 @@ /** * WordPress dependencies */ -import { useContext, useEffect, useState, useMemo } from '@wordpress/element'; +import { + useContext, + useEffect, + useState, + useMemo, + createInterpolateElement, +} from '@wordpress/element'; import { __experimentalSpacer as Spacer, __experimentalInputControl as InputControl, __experimentalText as Text, + __experimentalHStack as HStack, SelectControl, Spinner, Icon, @@ -14,7 +21,7 @@ import { Button, } from '@wordpress/components'; import { debounce } from '@wordpress/compose'; -import { __, _x } from '@wordpress/i18n'; +import { sprintf, __, _x } from '@wordpress/i18n'; import { search, closeSmall } from '@wordpress/icons'; /** @@ -22,7 +29,6 @@ import { search, closeSmall } from '@wordpress/icons'; */ import TabPanelLayout from './tab-panel-layout'; import { FontLibraryContext } from './context'; -import FontsGrid from './fonts-grid'; import FontCard from './font-card'; import filterFonts from './utils/filter-fonts'; import CollectionFontDetails from './collection-font-details'; @@ -48,6 +54,7 @@ function FontCollection( { slug } ) { const [ selectedFont, setSelectedFont ] = useState( null ); const [ fontsToInstall, setFontsToInstall ] = useState( [] ); + const [ page, setPage ] = useState( 1 ); const [ filters, setFilters ] = useState( {} ); const [ renderConfirmDialog, setRenderConfirmDialog ] = useState( requiresPermission && ! getGoogleFontsPermissionFromStorage() @@ -109,22 +116,34 @@ function FontCollection( { slug } ) { [ collectionFonts, filters ] ); + // NOTE: The height of the font library modal unavailable to use for rendering font family items is roughly 417px + // The height of each font family item is 61px. + const pageSize = Math.floor( ( window.innerHeight - 417 ) / 61 ); + const totalPages = Math.ceil( fonts.length / pageSize ); + const itemsStart = ( page - 1 ) * pageSize; + const itemsLimit = page * pageSize; + const items = fonts.slice( itemsStart, itemsLimit ); + const handleCategoryFilter = ( category ) => { setFilters( { ...filters, category } ); + setPage( 1 ); }; const handleUpdateSearchInput = ( value ) => { setFilters( { ...filters, search: value } ); + setPage( 1 ); }; const debouncedUpdateSearchInput = debounce( handleUpdateSearchInput, 300 ); const resetFilters = () => { setFilters( {} ); + setPage( 1 ); }; const resetSearch = () => { setFilters( { ...filters, search: '' } ); + setPage( 1 ); }; const handleUnselectFont = () => { @@ -186,6 +205,24 @@ function FontCollection( { slug } ) { resetFontsToInstall(); }; + let footerComponent = null; + if ( selectedFont ) { + footerComponent = ( + + ); + } else if ( ! renderConfirmDialog && totalPages > 1 ) { + footerComponent = ( + + ); + } + return ( - } + footer={ footerComponent } > { renderConfirmDialog && ( <> @@ -275,8 +307,8 @@ function FontCollection( { slug } ) { ) } { ! renderConfirmDialog && ! selectedFont && ( - - { fonts.map( ( font ) => ( +
    + { items.map( ( font ) => ( ) ) } - +
    ) }
    ); } -function Footer( { handleInstall, isDisabled } ) { +function PaginationFooter( { page, totalPages, setPage } ) { + return ( + + + + + { createInterpolateElement( + sprintf( + // translators: %s: Total number of pages. + _x( 'Page of %s', 'paging' ), + totalPages + ), + { + CurrenPageControl: ( + { + return { + label: i + 1, + value: i + 1, + }; + } + ) } + onChange={ ( newPage ) => + setPage( parseInt( newPage ) ) + } + size={ 'compact' } + __nextHasNoMarginBottom + /> + ), + } + ) } + + + + + ); +} + +function InstallFooter( { handleInstall, isDisabled } ) { const { isInstalling } = useContext( FontLibraryContext ); return ( diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/fonts-grid.js b/packages/edit-site/src/components/global-styles/font-library-modal/fonts-grid.js deleted file mode 100644 index 9700831a7adef1..00000000000000 --- a/packages/edit-site/src/components/global-styles/font-library-modal/fonts-grid.js +++ /dev/null @@ -1,59 +0,0 @@ -/** - * WordPress dependencies - */ -import { - __experimentalVStack as VStack, - __experimentalText as Text, - __experimentalSpacer as Spacer, -} from '@wordpress/components'; -import { useState, useEffect } from '@wordpress/element'; - -function FontsGrid( { title, children, pageSize = 32 } ) { - const [ lastItem, setLastItem ] = useState( null ); - const [ page, setPage ] = useState( 1 ); - const itemsLimit = page * pageSize; - const items = children.slice( 0, itemsLimit ); - - useEffect( () => { - if ( lastItem ) { - const observer = new window.IntersectionObserver( ( [ entry ] ) => { - if ( entry.isIntersecting ) { - setPage( ( prevPage ) => prevPage + 1 ); - } - } ); - - observer.observe( lastItem ); - - return () => observer.disconnect(); - } - }, [ lastItem ] ); - - return ( -
    - - { title && ( - <> - - { title } - - - - ) } -
    - { items.map( ( child, i ) => { - if ( i === itemsLimit - 1 ) { - return ( -
    - { child } -
    - ); - } - return
    { child }
    ; - } ) } -
    -
    -
    - ); -} - -export default FontsGrid; diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/installed-fonts.js b/packages/edit-site/src/components/global-styles/font-library-modal/installed-fonts.js index 73ff17f25b9a66..99c99e44a43afc 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/installed-fonts.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/installed-fonts.js @@ -7,6 +7,7 @@ import { privateApis as componentsPrivateApis, __experimentalHStack as HStack, __experimentalSpacer as Spacer, + __experimentalText as Text, Button, Spinner, FlexItem, @@ -17,7 +18,6 @@ import { */ import TabPanelLayout from './tab-panel-layout'; import { FontLibraryContext } from './context'; -import FontsGrid from './fonts-grid'; import LibraryFontDetails from './library-font-details'; import LibraryFontCard from './library-font-card'; import ConfirmDeleteDialog from './confirm-delete-dialog'; @@ -123,36 +123,38 @@ function InstalledFonts() { ) } { baseCustomFonts.length > 0 && ( <> - - { baseCustomFonts.map( ( font ) => ( - { - handleSelectFont( font ); - } } - /> - ) ) } - + { baseCustomFonts.map( ( font ) => ( + { + handleSelectFont( font ); + } } + /> + ) ) } ) } { baseThemeFonts.length > 0 && ( <> - - { baseThemeFonts.map( ( font ) => ( - { - handleSelectFont( font ); - } } - /> - ) ) } - + + { __( 'Theme Fonts' ) } + + + + { baseThemeFonts.map( ( font ) => ( + { + handleSelectFont( font ); + } } + /> + ) ) } ) } + ) } diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/style.scss b/packages/edit-site/src/components/global-styles/font-library-modal/style.scss index beb1ba46714ff7..544e3ed63c9883 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/style.scss +++ b/packages/edit-site/src/components/global-styles/font-library-modal/style.scss @@ -36,11 +36,8 @@ } } -.font-library-modal__fonts-grid { - .font-library-modal__fonts-grid__main { - display: flex; - flex-direction: column; - } +.font-library-modal__tabpanel-layout .components-base-control__field { + margin-bottom: 0; } .font-library-modal__font-card { From 7bfaf2c272ff7964641b8b00051969e36c9d3c47 Mon Sep 17 00:00:00 2001 From: Bernie Reiter <96308+ockham@users.noreply.github.com> Date: Tue, 13 Feb 2024 16:46:02 +0100 Subject: [PATCH 09/78] Revert "Block Hooks: Set ignoredHookedBlocks metada attr upon insertion (#58553)" (#58969) This reverts commit 7449074b46bcebb4724a4ffdfbb594ae2b3fc0de. --- .../reference-guides/data/data-core-blocks.md | 48 -------- packages/block-editor/src/store/selectors.js | 10 +- packages/blocks/README.md | 14 --- packages/blocks/src/api/index.js | 1 - packages/blocks/src/api/registration.js | 15 --- packages/blocks/src/api/templates.js | 31 +---- packages/blocks/src/api/test/templates.js | 80 +------------ packages/blocks/src/store/selectors.js | 62 ---------- packages/blocks/src/store/test/selectors.js | 106 ------------------ 9 files changed, 3 insertions(+), 364 deletions(-) diff --git a/docs/reference-guides/data/data-core-blocks.md b/docs/reference-guides/data/data-core-blocks.md index a25a521931e25a..084c9c1d7a5fbc 100644 --- a/docs/reference-guides/data/data-core-blocks.md +++ b/docs/reference-guides/data/data-core-blocks.md @@ -504,54 +504,6 @@ _Returns_ - `string?`: Name of the block for handling the grouping of blocks. -### getHookedBlocks - -Returns the hooked blocks for a given anchor block. - -Given an anchor block name, returns an object whose keys are relative positions, and whose values are arrays of block names that are hooked to the anchor block at that relative position. - -_Usage_ - -```js -import { store as blocksStore } from '@wordpress/blocks'; -import { useSelect } from '@wordpress/data'; - -const ExampleComponent = () => { - const hookedBlockNames = useSelect( - ( select ) => - select( blocksStore ).getHookedBlocks( 'core/navigation' ), - [] - ); - - return ( -
      - { Object.keys( hookedBlockNames ).length && - Object.keys( hookedBlockNames ).map( ( relativePosition ) => ( -
    • - { relativePosition }> -
        - { hookedBlockNames[ relativePosition ].map( - ( hookedBlock ) => ( -
      • { hookedBlock }
      • - ) - ) } -
      -
    • - ) ) } -
    - ); -}; -``` - -_Parameters_ - -- _state_ `Object`: Data state. -- _blockName_ `string`: Anchor block type name. - -_Returns_ - -- `Object`: Lists of hooked block names for each relative position. - ### getUnregisteredFallbackBlockName Returns the name of the block for handling unregistered blocks. diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index 373611cd3bd8e8..3475e2b5351c80 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -10,7 +10,6 @@ import { getBlockType, getBlockTypes, getBlockVariations, - getHookedBlocks, hasBlockSupport, getPossibleBlockTransformations, parse, @@ -1937,16 +1936,9 @@ const buildBlockTypeItem = blockType.name, 'inserter' ); - - const ignoredHookedBlocks = [ - ...new Set( Object.values( getHookedBlocks( id ) ).flat() ), - ]; - return { ...blockItemBase, - initialAttributes: ignoredHookedBlocks.length - ? { metadata: { ignoredHookedBlocks } } - : {}, + initialAttributes: {}, description: blockType.description, category: blockType.category, keywords: blockType.keywords, diff --git a/packages/blocks/README.md b/packages/blocks/README.md index eda3ac629bef87..8e6fdc9d900dbb 100644 --- a/packages/blocks/README.md +++ b/packages/blocks/README.md @@ -234,20 +234,6 @@ _Returns_ - `?string`: Block name. -### getHookedBlocks - -Returns the hooked blocks for a given anchor block. - -Given an anchor block name, returns an object whose keys are relative positions, and whose values are arrays of block names that are hooked to the anchor block at that relative position. - -_Parameters_ - -- _name_ `string`: Anchor block name. - -_Returns_ - -- `Object`: Lists of hooked block names for each relative position. - ### getPhrasingContentSchema Undocumented declaration. diff --git a/packages/blocks/src/api/index.js b/packages/blocks/src/api/index.js index 92738c6e16fbc3..2ddeb3a60f0abb 100644 --- a/packages/blocks/src/api/index.js +++ b/packages/blocks/src/api/index.js @@ -124,7 +124,6 @@ export { getBlockTypes, getBlockSupport, hasBlockSupport, - getHookedBlocks, getBlockVariations, isReusableBlock, isTemplatePart, diff --git a/packages/blocks/src/api/registration.js b/packages/blocks/src/api/registration.js index 71e59949d51d17..6633adf40050c5 100644 --- a/packages/blocks/src/api/registration.js +++ b/packages/blocks/src/api/registration.js @@ -550,21 +550,6 @@ export function hasBlockSupport( nameOrType, feature, defaultSupports ) { ); } -/** - * Returns the hooked blocks for a given anchor block. - * - * Given an anchor block name, returns an object whose keys are relative positions, - * and whose values are arrays of block names that are hooked to the anchor block - * at that relative position. - * - * @param {string} name Anchor block name. - * - * @return {Object} Lists of hooked block names for each relative position. - */ -export function getHookedBlocks( name ) { - return select( blocksStore ).getHookedBlocks( name ); -} - /** * Determines whether or not the given block is a reusable block. This is a * special block type that is used to point to a global block stored via the diff --git a/packages/blocks/src/api/templates.js b/packages/blocks/src/api/templates.js index 34e6954a9ff33f..bc76218892688a 100644 --- a/packages/blocks/src/api/templates.js +++ b/packages/blocks/src/api/templates.js @@ -8,7 +8,7 @@ import { renderToString } from '@wordpress/element'; */ import { convertLegacyBlockNameAndAttributes } from './parser/convert-legacy-block'; import { createBlock } from './factory'; -import { getBlockType, getHookedBlocks } from './registration'; +import { getBlockType } from './registration'; /** * Checks whether a list of blocks matches a template by comparing the block names. @@ -115,35 +115,6 @@ export function synchronizeBlocksWithTemplate( blocks = [], template ) { normalizedAttributes ); - const ignoredHookedBlocks = [ - ...new Set( - Object.values( getHookedBlocks( blockName ) ).flat() - ), - ]; - - if ( ignoredHookedBlocks.length ) { - const { metadata = {}, ...otherAttributes } = blockAttributes; - const { - ignoredHookedBlocks: ignoredHookedBlocksFromTemplate = [], - ...otherMetadata - } = metadata; - - const newIgnoredHookedBlocks = [ - ...new Set( [ - ...ignoredHookedBlocks, - ...ignoredHookedBlocksFromTemplate, - ] ), - ]; - - blockAttributes = { - metadata: { - ignoredHookedBlocks: newIgnoredHookedBlocks, - ...otherMetadata, - }, - ...otherAttributes, - }; - } - // If a Block is undefined at this point, use the core/missing block as // a placeholder for a better user experience. if ( undefined === getBlockType( blockName ) ) { diff --git a/packages/blocks/src/api/test/templates.js b/packages/blocks/src/api/test/templates.js index 8ee031aedbeefc..0a23505f0ac036 100644 --- a/packages/blocks/src/api/test/templates.js +++ b/packages/blocks/src/api/test/templates.js @@ -28,11 +28,7 @@ describe( 'templates', () => { beforeEach( () => { registerBlockType( 'core/test-block', { - attributes: { - metadata: { - type: 'object', - }, - }, + attributes: {}, save: noop, category: 'text', title: 'test block', @@ -136,80 +132,6 @@ describe( 'templates', () => { ] ); } ); - it( 'should set ignoredHookedBlocks metadata if a block has hooked blocks', () => { - registerBlockType( 'core/hooked-block', { - attributes: {}, - save: noop, - category: 'text', - title: 'hooked block', - blockHooks: { 'core/test-block': 'after' }, - } ); - - const template = [ - [ 'core/test-block' ], - [ 'core/test-block-2' ], - [ 'core/test-block-2' ], - ]; - const blockList = []; - - expect( - synchronizeBlocksWithTemplate( blockList, template ) - ).toMatchObject( [ - { - name: 'core/test-block', - attributes: { - metadata: { - ignoredHookedBlocks: [ 'core/hooked-block' ], - }, - }, - }, - { name: 'core/test-block-2' }, - { name: 'core/test-block-2' }, - ] ); - } ); - - it( 'retains previously set ignoredHookedBlocks metadata', () => { - registerBlockType( 'core/hooked-block', { - attributes: {}, - save: noop, - category: 'text', - title: 'hooked block', - blockHooks: { 'core/test-block': 'after' }, - } ); - - const template = [ - [ - 'core/test-block', - { - metadata: { - ignoredHookedBlocks: [ 'core/other-hooked-block' ], - }, - }, - ], - [ 'core/test-block-2' ], - [ 'core/test-block-2' ], - ]; - const blockList = []; - - expect( - synchronizeBlocksWithTemplate( blockList, template ) - ).toMatchObject( [ - { - name: 'core/test-block', - attributes: { - metadata: { - ignoredHookedBlocks: [ - 'core/hooked-block', - 'core/other-hooked-block', - ], - }, - }, - }, - { name: 'core/test-block-2' }, - { name: 'core/test-block-2' }, - ] ); - } ); - it( 'should create nested blocks', () => { const template = [ [ 'core/test-block', {}, [ [ 'core/test-block-2' ] ] ], diff --git a/packages/blocks/src/store/selectors.js b/packages/blocks/src/store/selectors.js index 9eda135d0d6999..b2b8ab8106f097 100644 --- a/packages/blocks/src/store/selectors.js +++ b/packages/blocks/src/store/selectors.js @@ -106,68 +106,6 @@ export function getBlockType( state, name ) { return state.blockTypes[ name ]; } -/** - * Returns the hooked blocks for a given anchor block. - * - * Given an anchor block name, returns an object whose keys are relative positions, - * and whose values are arrays of block names that are hooked to the anchor block - * at that relative position. - * - * @param {Object} state Data state. - * @param {string} blockName Anchor block type name. - * - * @example - * ```js - * import { store as blocksStore } from '@wordpress/blocks'; - * import { useSelect } from '@wordpress/data'; - * - * const ExampleComponent = () => { - * const hookedBlockNames = useSelect( ( select ) => - * select( blocksStore ).getHookedBlocks( 'core/navigation' ), - * [] - * ); - * - * return ( - *
      - * { Object.keys( hookedBlockNames ).length && - * Object.keys( hookedBlockNames ).map( ( relativePosition ) => ( - *
    • { relativePosition }> - *
        - * { hookedBlockNames[ relativePosition ].map( ( hookedBlock ) => ( - *
      • { hookedBlock }
      • - * ) ) } - *
      - *
    • - * ) ) } - *
    - * ); - * }; - * ``` - * - * @return {Object} Lists of hooked block names for each relative position. - */ -export const getHookedBlocks = createSelector( - ( state, blockName ) => { - const hookedBlockTypes = getBlockTypes( state ).filter( - ( { blockHooks } ) => blockHooks && blockName in blockHooks - ); - - let hookedBlocks = {}; - for ( const blockType of hookedBlockTypes ) { - const relativePosition = blockType.blockHooks[ blockName ]; - hookedBlocks = { - ...hookedBlocks, - [ relativePosition ]: [ - ...( hookedBlocks[ relativePosition ] ?? [] ), - blockType.name, - ], - }; - } - return hookedBlocks; - }, - ( state ) => [ state.blockTypes ] -); - /** * Returns block styles by block name. * diff --git a/packages/blocks/src/store/test/selectors.js b/packages/blocks/src/store/test/selectors.js index 0dcdde3c07bf10..1fda11d72311a3 100644 --- a/packages/blocks/src/store/test/selectors.js +++ b/packages/blocks/src/store/test/selectors.js @@ -12,7 +12,6 @@ import { getBlockVariations, getDefaultBlockVariation, getGroupingBlockName, - getHookedBlocks, isMatchingSearchTerm, getCategories, getActiveBlockVariation, @@ -229,111 +228,6 @@ describe( 'selectors', () => { } ); } ); - describe( 'getHookedBlocks', () => { - it( 'should return an empty object if state is empty', () => { - const state = { - blockTypes: {}, - }; - - expect( getHookedBlocks( state, 'anchor' ) ).toEqual( {} ); - } ); - - it( 'should return an empty object if the anchor block is not found', () => { - const state = { - blockTypes: { - anchor: { - name: 'anchor', - }, - hookedBlock: { - name: 'hookedBlock', - blockHooks: { - anchor: 'after', - }, - }, - }, - }; - - expect( getHookedBlocks( state, 'otherAnchor' ) ).toEqual( {} ); - } ); - - it( "should return the anchor block name even if the anchor block doesn't exist", () => { - const state = { - blockTypes: { - hookedBlock: { - name: 'hookedBlock', - blockHooks: { - anchor: 'after', - }, - }, - }, - }; - - expect( getHookedBlocks( state, 'anchor' ) ).toEqual( { - after: [ 'hookedBlock' ], - } ); - } ); - - it( 'should return an array with the hooked block names', () => { - const state = { - blockTypes: { - anchor: { - name: 'anchor', - }, - hookedBlock1: { - name: 'hookedBlock1', - blockHooks: { - anchor: 'after', - }, - }, - hookedBlock2: { - name: 'hookedBlock2', - blockHooks: { - anchor: 'before', - }, - }, - }, - }; - - expect( getHookedBlocks( state, 'anchor' ) ).toEqual( { - after: [ 'hookedBlock1' ], - before: [ 'hookedBlock2' ], - } ); - } ); - - it( 'should return an array with the hooked block names, even if multiple blocks are in the same relative position', () => { - const state = { - blockTypes: { - anchor: { - name: 'anchor', - }, - hookedBlock1: { - name: 'hookedBlock1', - blockHooks: { - anchor: 'after', - }, - }, - hookedBlock2: { - name: 'hookedBlock2', - blockHooks: { - anchor: 'before', - }, - }, - hookedBlock3: { - name: 'hookedBlock3', - blockHooks: { - anchor: 'after', - }, - }, - }, - }; - - expect( getHookedBlocks( state, 'anchor' ) ).toEqual( { - after: [ 'hookedBlock1', 'hookedBlock3' ], - before: [ 'hookedBlock2' ], - } ); - } ); - } ); - describe( 'Testing block variations selectors', () => { const blockName = 'block/name'; const createBlockVariationsState = ( variations ) => { From 0c6f3cb36cd979e058a568e25afde4a6c30523ff Mon Sep 17 00:00:00 2001 From: Nik Tsekouras Date: Tue, 13 Feb 2024 17:49:02 +0200 Subject: [PATCH 10/78] DataViews: Fix patterns, templates and template parts pagination `z-index` (#58965) Co-authored-by: ntsekouras Co-authored-by: t-hamano Co-authored-by: jameskoster Co-authored-by: annezazu Co-authored-by: miksansegundo --- packages/base-styles/_z-index.scss | 3 ++- packages/edit-site/src/components/page-patterns/style.scss | 4 ++++ .../src/components/page-templates-template-parts/index.js | 1 + .../src/components/page-templates-template-parts/style.scss | 6 ++++++ 4 files changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/base-styles/_z-index.scss b/packages/base-styles/_z-index.scss index e6d8d12769e701..ff21d1d8df8f35 100644 --- a/packages/base-styles/_z-index.scss +++ b/packages/base-styles/_z-index.scss @@ -190,7 +190,8 @@ $z-layers: ( ".edit-site-page-header": 2, ".edit-site-page-content": 1, ".edit-site-patterns__header": 2, - ".edit-site-patterns__grid-pagination": 2, + ".edit-site-patterns__dataviews-list-pagination": 2, + ".edit-site-templates__dataviews-list-pagination": 2, ".edit-site-layout__canvas-container": 2, ".edit-site-layout__sidebar": 1, ".edit-site-layout__canvas-container.is-resizing::after": 100, diff --git a/packages/edit-site/src/components/page-patterns/style.scss b/packages/edit-site/src/components/page-patterns/style.scss index 89f38e7d4c9a63..2dc9326e4d29be 100644 --- a/packages/edit-site/src/components/page-patterns/style.scss +++ b/packages/edit-site/src/components/page-patterns/style.scss @@ -101,6 +101,10 @@ text-overflow: ellipsis; color: inherit; } + + .dataviews-pagination { + z-index: z-index(".edit-site-patterns__dataviews-list-pagination"); + } } .dataviews-action-modal__duplicate-pattern { diff --git a/packages/edit-site/src/components/page-templates-template-parts/index.js b/packages/edit-site/src/components/page-templates-template-parts/index.js index 514ff148071955..9089d31cd66e0a 100644 --- a/packages/edit-site/src/components/page-templates-template-parts/index.js +++ b/packages/edit-site/src/components/page-templates-template-parts/index.js @@ -429,6 +429,7 @@ export default function PageTemplatesTemplateParts( { postType } ) { return ( Date: Tue, 13 Feb 2024 17:49:07 +0100 Subject: [PATCH 11/78] Block Bindings: Fix disable bindings editing when source is undefined (#58961) * Disable editing if source is undefined * Add tests when source is undefined --- .../src/components/rich-text/index.js | 17 +- packages/block-library/src/button/edit.js | 8 +- packages/block-library/src/image/edit.js | 8 +- packages/block-library/src/image/image.js | 21 +- .../editor/various/block-bindings.spec.js | 453 +++++++++++++++++- 5 files changed, 480 insertions(+), 27 deletions(-) diff --git a/packages/block-editor/src/components/rich-text/index.js b/packages/block-editor/src/components/rich-text/index.js index 43a9fb1a31f1bf..deafde69b35c37 100644 --- a/packages/block-editor/src/components/rich-text/index.js +++ b/packages/block-editor/src/components/rich-text/index.js @@ -156,12 +156,19 @@ export function RichTextWrapper( for ( const [ attribute, args ] of Object.entries( blockBindings ) ) { - // If any of the attributes with source "rich-text" is part of the bindings, - // has a source with `lockAttributesEditing`, disable it. if ( - blockTypeAttributes?.[ attribute ]?.source === - 'rich-text' && - getBlockBindingsSource( args.source )?.lockAttributesEditing + blockTypeAttributes?.[ attribute ]?.source !== 'rich-text' + ) { + break; + } + + // If the source is not defined, or if its value of `lockAttributesEditing` is `true`, disable it. + const blockBindingsSource = getBlockBindingsSource( + args.source + ); + if ( + ! blockBindingsSource || + blockBindingsSource.lockAttributesEditing ) { shouldDisableEditing = true; break; diff --git a/packages/block-library/src/button/edit.js b/packages/block-library/src/button/edit.js index e01898ca00dec4..ff90cdd1bf64c0 100644 --- a/packages/block-library/src/button/edit.js +++ b/packages/block-library/src/button/edit.js @@ -238,15 +238,15 @@ function ButtonEdit( props ) { return {}; } - const { getBlockBindingsSource } = unlock( + const blockBindingsSource = unlock( select( blockEditorStore ) - ); + ).getBlockBindingsSource( metadata?.bindings?.url?.source ); return { lockUrlControls: !! metadata?.bindings?.url && - getBlockBindingsSource( metadata?.bindings?.url?.source ) - ?.lockAttributesEditing, + ( ! blockBindingsSource || + blockBindingsSource?.lockAttributesEditing ), }; }, [ isSelected ] diff --git a/packages/block-library/src/image/edit.js b/packages/block-library/src/image/edit.js index 61d023e4e580a1..86970b588ff03d 100644 --- a/packages/block-library/src/image/edit.js +++ b/packages/block-library/src/image/edit.js @@ -341,15 +341,15 @@ export function ImageEdit( { return {}; } - const { getBlockBindingsSource } = unlock( + const blockBindingsSource = unlock( select( blockEditorStore ) - ); + ).getBlockBindingsSource( metadata?.bindings?.url?.source ); return { lockUrlControls: !! metadata?.bindings?.url && - getBlockBindingsSource( metadata?.bindings?.url?.source ) - ?.lockAttributesEditing, + ( ! blockBindingsSource || + blockBindingsSource?.lockAttributesEditing ), }; }, [ isSingleSelected ] diff --git a/packages/block-library/src/image/image.js b/packages/block-library/src/image/image.js index f551d8df007a8e..f188f8eaaf3101 100644 --- a/packages/block-library/src/image/image.js +++ b/packages/block-library/src/image/image.js @@ -423,23 +423,32 @@ export default function Image( { } = metadata?.bindings || {}; const hasParentPattern = getBlockParentsByBlockName( clientId, 'core/block' ).length > 0; + const urlBindingSource = getBlockBindingsSource( + urlBinding?.source + ); + const altBindingSource = getBlockBindingsSource( + altBinding?.source + ); + const titleBindingSource = getBlockBindingsSource( + titleBinding?.source + ); return { lockUrlControls: !! urlBinding && - getBlockBindingsSource( urlBinding?.source ) - ?.lockAttributesEditing, + ( ! urlBindingSource || + urlBindingSource?.lockAttributesEditing ), lockHrefControls: // Disable editing the link of the URL if the image is inside a pattern instance. // This is a temporary solution until we support overriding the link on the frontend. hasParentPattern, lockAltControls: !! altBinding && - getBlockBindingsSource( altBinding?.source ) - ?.lockAttributesEditing, + ( ! altBindingSource || + altBindingSource?.lockAttributesEditing ), lockTitleControls: !! titleBinding && - getBlockBindingsSource( titleBinding?.source ) - ?.lockAttributesEditing, + ( ! titleBindingSource || + titleBindingSource?.lockAttributesEditing ), }; }, [ clientId, isSingleSelected, metadata?.bindings ] diff --git a/test/e2e/specs/editor/various/block-bindings.spec.js b/test/e2e/specs/editor/various/block-bindings.spec.js index ccd115e79e8d82..d98a4eda512651 100644 --- a/test/e2e/specs/editor/various/block-bindings.spec.js +++ b/test/e2e/specs/editor/various/block-bindings.spec.js @@ -72,7 +72,7 @@ test.describe( 'Block bindings', () => { ); } ); - test( 'Should lock the appropriate controls', async ( { + test( 'Should lock the appropriate controls with a registered source', async ( { editor, page, } ) => { @@ -117,6 +117,52 @@ test.describe( 'Block bindings', () => { 'false' ); } ); + + test( 'Should lock the appropriate controls when source is not defined', async ( { + editor, + page, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + content: 'paragraph default content', + metadata: { + bindings: { + content: { + source: 'plugin/undefined-source', + args: { key: 'text_custom_field' }, + }, + }, + }, + }, + } ); + const paragraphBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Paragraph', + } ); + await paragraphBlock.click(); + + // Alignment controls exist. + await expect( + page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'button', { name: 'Align text' } ) + ).toBeVisible(); + + // Format controls don't exist. + await expect( + page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'button', { + name: 'Bold', + } ) + ).toBeHidden(); + + // Paragraph is not editable. + await expect( paragraphBlock ).toHaveAttribute( + 'contenteditable', + 'false' + ); + } ); } ); test.describe( 'Heading', () => { @@ -143,7 +189,7 @@ test.describe( 'Block bindings', () => { await expect( headingBlock ).toHaveText( 'text_custom_field' ); } ); - test( 'Should lock the appropriate controls', async ( { + test( 'Should lock the appropriate controls with a registered source', async ( { editor, page, } ) => { @@ -188,6 +234,52 @@ test.describe( 'Block bindings', () => { 'false' ); } ); + + test( 'Should lock the appropriate controls when source is not defined', async ( { + editor, + page, + } ) => { + await editor.insertBlock( { + name: 'core/heading', + attributes: { + content: 'heading default content', + metadata: { + bindings: { + content: { + source: 'plugin/undefined-source', + args: { key: 'text_custom_field' }, + }, + }, + }, + }, + } ); + const headingBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Heading', + } ); + await headingBlock.click(); + + // Alignment controls exist. + await expect( + page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'button', { name: 'Align text' } ) + ).toBeVisible(); + + // Format controls don't exist. + await expect( + page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'button', { + name: 'Bold', + } ) + ).toBeHidden(); + + // Heading is not editable. + await expect( headingBlock ).toHaveAttribute( + 'contenteditable', + 'false' + ); + } ); } ); test.describe( 'Button', () => { @@ -221,7 +313,7 @@ test.describe( 'Block bindings', () => { await expect( buttonBlock ).toHaveText( 'text_custom_field' ); } ); - test( 'Should lock text controls when text is bound', async ( { + test( 'Should lock text controls when text is bound to a registered source', async ( { editor, page, } ) => { @@ -283,7 +375,69 @@ test.describe( 'Block bindings', () => { ).toBeVisible(); } ); - test( 'Should lock url controls when url is bound', async ( { + test( 'Should lock text controls when text is bound to an undefined source', async ( { + editor, + page, + } ) => { + await editor.insertBlock( { + name: 'core/buttons', + innerBlocks: [ + { + name: 'core/button', + attributes: { + text: 'button default text', + url: '#default-url', + metadata: { + bindings: { + text: { + source: 'plugin/undefined-source', + args: { key: 'text_custom_field' }, + }, + }, + }, + }, + }, + ], + } ); + const buttonBlock = editor.canvas + .getByRole( 'document', { + name: 'Block: Button', + exact: true, + } ) + .getByRole( 'textbox' ); + await buttonBlock.click(); + + // Alignment controls exist. + await expect( + page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'button', { name: 'Align text' } ) + ).toBeVisible(); + + // Format controls don't exist. + await expect( + page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'button', { + name: 'Bold', + } ) + ).toBeHidden(); + + // Button is not editable. + await expect( buttonBlock ).toHaveAttribute( + 'contenteditable', + 'false' + ); + + // Link controls exist. + await expect( + page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'button', { name: 'Unlink' } ) + ).toBeVisible(); + } ); + + test( 'Should lock url controls when url is bound to a registered source', async ( { editor, page, } ) => { @@ -343,6 +497,66 @@ test.describe( 'Block bindings', () => { ).toBeHidden(); } ); + test( 'Should lock url controls when url is bound to an undefined source', async ( { + editor, + page, + } ) => { + await editor.insertBlock( { + name: 'core/buttons', + innerBlocks: [ + { + name: 'core/button', + attributes: { + text: 'button default text', + url: '#default-url', + metadata: { + bindings: { + url: { + source: 'plugin/undefined-source', + args: { key: 'url_custom_field' }, + }, + }, + }, + }, + }, + ], + } ); + const buttonBlock = editor.canvas + .getByRole( 'document', { + name: 'Block: Button', + exact: true, + } ) + .getByRole( 'textbox' ); + await buttonBlock.click(); + + // Format controls exist. + await expect( + page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'button', { + name: 'Bold', + } ) + ).toBeVisible(); + + // Button is editable. + await expect( buttonBlock ).toHaveAttribute( + 'contenteditable', + 'true' + ); + + // Link controls don't exist. + await expect( + page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'button', { name: 'Link' } ) + ).toBeHidden(); + await expect( + page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'button', { name: 'Unlink' } ) + ).toBeHidden(); + } ); + test( 'Should lock url and text controls when both are bound', async ( { editor, page, @@ -429,7 +643,7 @@ test.describe( 'Block bindings', () => { ).toBeVisible(); } ); - test( 'Should NOT show the upload form when url is bound', async ( { + test( 'Should NOT show the upload form when url is bound to a registered source', async ( { editor, } ) => { await editor.insertBlock( { @@ -457,7 +671,35 @@ test.describe( 'Block bindings', () => { ).toBeHidden(); } ); - test( 'Should lock url controls when url is bound', async ( { + test( 'Should NOT show the upload form when url is bound to an undefined source', async ( { + editor, + } ) => { + await editor.insertBlock( { + name: 'core/image', + attributes: { + url: imagePlaceholderSrc, + alt: 'default alt value', + title: 'default title value', + metadata: { + bindings: { + url: { + source: 'plugin/undefined-source', + args: { key: 'url_custom_field' }, + }, + }, + }, + }, + } ); + const imageBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Image', + } ); + await imageBlock.click(); + await expect( + imageBlock.getByRole( 'button', { name: 'Upload' } ) + ).toBeHidden(); + } ); + + test( 'Should lock url controls when url is bound to a registered source', async ( { editor, page, } ) => { @@ -526,7 +768,76 @@ test.describe( 'Block bindings', () => { expect( titleValue ).toBe( 'default title value' ); } ); - test( 'Should disable alt textarea when alt is bound', async ( { + test( 'Should lock url controls when url is bound to an undefined source', async ( { + editor, + page, + } ) => { + await editor.insertBlock( { + name: 'core/image', + attributes: { + url: imagePlaceholderSrc, + alt: 'default alt value', + title: 'default title value', + metadata: { + bindings: { + url: { + source: 'plugin/undefined-source', + args: { key: 'url_custom_field' }, + }, + }, + }, + }, + } ); + const imageBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Image', + } ); + await imageBlock.click(); + + // Replace controls don't exist. + await expect( + page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'button', { + name: 'Replace', + } ) + ).toBeHidden(); + + // Image placeholder doesn't show the upload button. + await expect( + imageBlock.getByRole( 'button', { name: 'Upload' } ) + ).toBeHidden(); + + // Alt textarea is enabled and with the original value. + await expect( + page + .getByRole( 'tabpanel', { name: 'Settings' } ) + .getByLabel( 'Alternative text' ) + ).toBeEnabled(); + const altValue = await page + .getByRole( 'tabpanel', { name: 'Settings' } ) + .getByLabel( 'Alternative text' ) + .inputValue(); + expect( altValue ).toBe( 'default alt value' ); + + // Title input is enabled and with the original value. + await page + .getByRole( 'tabpanel', { name: 'Settings' } ) + .getByRole( 'button', { name: 'Advanced' } ) + .click(); + + await expect( + page + .getByRole( 'tabpanel', { name: 'Settings' } ) + .getByLabel( 'Title attribute' ) + ).toBeEnabled(); + const titleValue = await page + .getByRole( 'tabpanel', { name: 'Settings' } ) + .getByLabel( 'Title attribute' ) + .inputValue(); + expect( titleValue ).toBe( 'default title value' ); + } ); + + test( 'Should disable alt textarea when alt is bound to a registered source', async ( { editor, page, } ) => { @@ -589,7 +900,70 @@ test.describe( 'Block bindings', () => { expect( titleValue ).toBe( 'default title value' ); } ); - test( 'Should disable title input when title is bound', async ( { + test( 'Should disable alt textarea when alt is bound to an undefined source', async ( { + editor, + page, + } ) => { + await editor.insertBlock( { + name: 'core/image', + attributes: { + url: imagePlaceholderSrc, + alt: 'default alt value', + title: 'default title value', + metadata: { + bindings: { + alt: { + source: 'plguin/undefined-source', + args: { key: 'text_custom_field' }, + }, + }, + }, + }, + } ); + const imageBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Image', + } ); + await imageBlock.click(); + + // Replace controls exist. + await expect( + page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'button', { + name: 'Replace', + } ) + ).toBeVisible(); + + // Alt textarea is disabled and with the custom field value. + await expect( + page + .getByRole( 'tabpanel', { name: 'Settings' } ) + .getByLabel( 'Alternative text' ) + ).toBeDisabled(); + const altValue = await page + .getByRole( 'tabpanel', { name: 'Settings' } ) + .getByLabel( 'Alternative text' ) + .inputValue(); + expect( altValue ).toBe( 'default alt value' ); + + // Title input is enabled and with the original value. + await page + .getByRole( 'tabpanel', { name: 'Settings' } ) + .getByRole( 'button', { name: 'Advanced' } ) + .click(); + await expect( + page + .getByRole( 'tabpanel', { name: 'Settings' } ) + .getByLabel( 'Title attribute' ) + ).toBeEnabled(); + const titleValue = await page + .getByRole( 'tabpanel', { name: 'Settings' } ) + .getByLabel( 'Title attribute' ) + .inputValue(); + expect( titleValue ).toBe( 'default title value' ); + } ); + + test( 'Should disable title input when title is bound to a registered source', async ( { editor, page, } ) => { @@ -652,6 +1026,69 @@ test.describe( 'Block bindings', () => { expect( titleValue ).toBe( 'text_custom_field' ); } ); + test( 'Should disable title input when title is bound to an undefined source', async ( { + editor, + page, + } ) => { + await editor.insertBlock( { + name: 'core/image', + attributes: { + url: imagePlaceholderSrc, + alt: 'default alt value', + title: 'default title value', + metadata: { + bindings: { + title: { + source: 'plugin/undefined-source', + args: { key: 'text_custom_field' }, + }, + }, + }, + }, + } ); + const imageBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Image', + } ); + await imageBlock.click(); + + // Replace controls exist. + await expect( + page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'button', { + name: 'Replace', + } ) + ).toBeVisible(); + + // Alt textarea is enabled and with the original value. + await expect( + page + .getByRole( 'tabpanel', { name: 'Settings' } ) + .getByLabel( 'Alternative text' ) + ).toBeEnabled(); + const altValue = await page + .getByRole( 'tabpanel', { name: 'Settings' } ) + .getByLabel( 'Alternative text' ) + .inputValue(); + expect( altValue ).toBe( 'default alt value' ); + + // Title input is disabled and with the custom field value. + await page + .getByRole( 'tabpanel', { name: 'Settings' } ) + .getByRole( 'button', { name: 'Advanced' } ) + .click(); + await expect( + page + .getByRole( 'tabpanel', { name: 'Settings' } ) + .getByLabel( 'Title attribute' ) + ).toBeDisabled(); + const titleValue = await page + .getByRole( 'tabpanel', { name: 'Settings' } ) + .getByLabel( 'Title attribute' ) + .inputValue(); + expect( titleValue ).toBe( 'default title value' ); + } ); + test( 'Multiple bindings should lock the appropriate controls', async ( { editor, page, From f790549936280903c7bd43c9ce2e81f3769f265d Mon Sep 17 00:00:00 2001 From: Ramon Date: Wed, 14 Feb 2024 08:16:28 +1100 Subject: [PATCH 12/78] Style engine: rename at_rule to rules_groups and update test/docs (#58922) * Add missing tests Update var name and documentation * linty * Update test to reflect merge functionality * Return type fix unit test comment * Return type again fix unit test comment again Co-authored-by: ramonjd Co-authored-by: andrewserong Co-authored-by: tellthemachines --- packages/style-engine/README.md | 25 +++++++- .../class-wp-style-engine-css-rule.php | 47 +++++++------- .../class-wp-style-engine-css-rules-store.php | 18 +++--- .../class-wp-style-engine-processor.php | 12 ++-- .../style-engine/class-wp-style-engine.php | 5 +- packages/style-engine/style-engine.php | 9 ++- .../class-wp-style-engine-css-rule-test.php | 16 +++++ ...s-wp-style-engine-css-rules-store-test.php | 16 +++++ .../class-wp-style-engine-processor-test.php | 8 +-- phpunit/style-engine/style-engine-test.php | 64 +++++++++++++++++-- 10 files changed, 164 insertions(+), 56 deletions(-) diff --git a/packages/style-engine/README.md b/packages/style-engine/README.md index 290e04ee60ae83..f26f8ff7e779b0 100644 --- a/packages/style-engine/README.md +++ b/packages/style-engine/README.md @@ -126,10 +126,31 @@ $styles = array( 'selector' => '.wp-tomato', 'declarations' => array( 'padding' => '100px' ) ), +); + +$stylesheet = wp_style_engine_get_stylesheet_from_css_rules( + $styles, array( - 'selector' => '.wp-kumquat', + 'context' => 'block-supports', // Indicates that these styles should be stored with block supports CSS. + ) +); +print_r( $stylesheet ); // .wp-pumpkin{color:orange}.wp-tomato{color:red;padding:100px} +``` + +It's also possible to build simple, nested CSS rules using the `rules_group` key. + +```php +$styles = array( + array( + 'rules_group' => '@media (min-width: 80rem)', + 'selector' => '.wp-carrot', 'declarations' => array( 'color' => 'orange' ) ), + array( + 'rules_group' => '@media (min-width: 80rem)', + 'selector' => '.wp-tomato', + 'declarations' => array( 'color' => 'red' ) + ), ); $stylesheet = wp_style_engine_get_stylesheet_from_css_rules( @@ -138,7 +159,7 @@ $stylesheet = wp_style_engine_get_stylesheet_from_css_rules( 'context' => 'block-supports', // Indicates that these styles should be stored with block supports CSS. ) ); -print_r( $stylesheet ); // .wp-pumpkin,.wp-kumquat{color:orange}.wp-tomato{color:red;padding:100px} +print_r( $stylesheet ); // @media (min-width: 80rem){.wp-carrot{color:orange}}@media (min-width: 80rem){.wp-tomato{color:red;}} ``` ### wp_style_engine_get_stylesheet_from_context() diff --git a/packages/style-engine/class-wp-style-engine-css-rule.php b/packages/style-engine/class-wp-style-engine-css-rule.php index 18d8c2f7ef7f0f..12fe5fab23f723 100644 --- a/packages/style-engine/class-wp-style-engine-css-rule.php +++ b/packages/style-engine/class-wp-style-engine-css-rule.php @@ -33,12 +33,11 @@ class WP_Style_Engine_CSS_Rule { protected $declarations; /** - * The CSS nested @rule, such as `@media (min-width: 80rem)` or `@layer module`. + * A parent CSS selector in the case of nested CSS, or a CSS nested @rule, such as `@media (min-width: 80rem)` or `@layer module`.. * * @var string */ - protected $at_rule; - + protected $rules_group; /** * Constructor @@ -46,13 +45,13 @@ class WP_Style_Engine_CSS_Rule { * @param string $selector The CSS selector. * @param string[]|WP_Style_Engine_CSS_Declarations $declarations An associative array of CSS definitions, e.g., array( "$property" => "$value", "$property" => "$value" ), * or a WP_Style_Engine_CSS_Declarations object. - * @param string $at_rule A CSS nested @rule, such as `@media (min-width: 80rem)` or `@layer module`. + * @param string $rules_group A parent CSS selector in the case of nested CSS, or a CSS nested @rule, such as `@media (min-width: 80rem)` or `@layer module`. * */ - public function __construct( $selector = '', $declarations = array(), $at_rule = '' ) { + public function __construct( $selector = '', $declarations = array(), $rules_group = '' ) { $this->set_selector( $selector ); $this->add_declarations( $declarations ); - $this->set_at_rule( $at_rule ); + $this->set_rules_group( $rules_group ); } /** @@ -92,17 +91,26 @@ public function add_declarations( $declarations ) { } /** - * Sets the at_rule. + * Sets the rules group. * - * @param string $at_rule A CSS nested @rule, such as `@media (min-width: 80rem)` or `@layer module`. + * @param string $rules_group A parent CSS selector in the case of nested CSS, or a CSS nested @rule, such as `@media (min-width: 80rem)` or `@layer module`. * * @return WP_Style_Engine_CSS_Rule Returns the object to allow chaining of methods. */ - public function set_at_rule( $at_rule ) { - $this->at_rule = $at_rule; + public function set_rules_group( $rules_group ) { + $this->rules_group = $rules_group; return $this; } + /** + * Gets the rules group. + * + * @return string + */ + public function get_rules_group() { + return $this->rules_group; + } + /** * Gets the declarations object. * @@ -121,15 +129,6 @@ public function get_selector() { return $this->selector; } - /** - * Gets the at_rule. - * - * @return string - */ - public function get_at_rule() { - return $this->at_rule; - } - /** * Gets the CSS. * @@ -148,16 +147,16 @@ public function get_css( $should_prettify = false, $indent_count = 0 ) { // Trims any multiple selectors strings. $selector = $should_prettify ? implode( ',', array_map( 'trim', explode( ',', $this->get_selector() ) ) ) : $this->get_selector(); $selector = $should_prettify ? str_replace( array( ',' ), ",\n", $selector ) : $selector; - $at_rule = $this->get_at_rule(); - $has_at_rule = ! empty( $at_rule ); - $css_declarations = $this->declarations->get_declarations_string( $should_prettify, $has_at_rule ? $nested_declarations_indent : $declarations_indent ); + $rules_group = $this->get_rules_group(); + $has_rules_group = ! empty( $rules_group ); + $css_declarations = $this->declarations->get_declarations_string( $should_prettify, $has_rules_group ? $nested_declarations_indent : $declarations_indent ); if ( empty( $css_declarations ) ) { return ''; } - if ( $has_at_rule ) { - $selector = "{$rule_indent}{$at_rule}{$spacer}{{$suffix}{$nested_rule_indent}{$selector}{$spacer}{{$suffix}{$css_declarations}{$suffix}{$nested_rule_indent}}{$suffix}{$rule_indent}}"; + if ( $has_rules_group ) { + $selector = "{$rule_indent}{$rules_group}{$spacer}{{$suffix}{$nested_rule_indent}{$selector}{$spacer}{{$suffix}{$css_declarations}{$suffix}{$nested_rule_indent}}{$suffix}{$rule_indent}}"; return $selector; } diff --git a/packages/style-engine/class-wp-style-engine-css-rules-store.php b/packages/style-engine/class-wp-style-engine-css-rules-store.php index e199cda5da0fcb..50d1cff4142523 100644 --- a/packages/style-engine/class-wp-style-engine-css-rules-store.php +++ b/packages/style-engine/class-wp-style-engine-css-rules-store.php @@ -109,25 +109,25 @@ public function get_all_rules() { * Gets a WP_Style_Engine_CSS_Rule object by its selector. * If the rule does not exist, it will be created. * - * @param string $selector The CSS selector. - * @param string $at_rule The CSS nested @rule, such as `@media (min-width: 80rem)` or `@layer module`. + * @param string $selector The CSS selector. + * @param string $rules_group A parent CSS selector in the case of nested CSS, or a CSS nested @rule, such as `@media (min-width: 80rem)` or `@layer module`.. * * @return WP_Style_Engine_CSS_Rule|void Returns a WP_Style_Engine_CSS_Rule object, or null if the selector is empty. */ - public function add_rule( $selector, $at_rule = '' ) { - $selector = trim( $selector ); - $at_rule = trim( $at_rule ); + public function add_rule( $selector, $rules_group = '' ) { + $selector = $selector ? trim( $selector ) : ''; + $rules_group = $rules_group ? trim( $rules_group ) : ''; // Bail early if there is no selector. if ( empty( $selector ) ) { return; } - if ( ! empty( $at_rule ) ) { - if ( empty( $this->rules[ "$at_rule $selector" ] ) ) { - $this->rules[ "$at_rule $selector" ] = new WP_Style_Engine_CSS_Rule( $selector, array(), $at_rule ); + if ( ! empty( $rules_group ) ) { + if ( empty( $this->rules[ "$rules_group $selector" ] ) ) { + $this->rules[ "$rules_group $selector" ] = new WP_Style_Engine_CSS_Rule( $selector, array(), $rules_group ); } - return $this->rules[ "$at_rule $selector" ]; + return $this->rules[ "$rules_group $selector" ]; } // Create the rule if it doesn't exist. diff --git a/packages/style-engine/class-wp-style-engine-processor.php b/packages/style-engine/class-wp-style-engine-processor.php index 3f8cef0a2cf315..728b8029f26900 100644 --- a/packages/style-engine/class-wp-style-engine-processor.php +++ b/packages/style-engine/class-wp-style-engine-processor.php @@ -65,20 +65,20 @@ public function add_rules( $css_rules ) { } foreach ( $css_rules as $rule ) { - $selector = $rule->get_selector(); - $at_rule = $rule->get_at_rule(); + $selector = $rule->get_selector(); + $rules_group = $rule->get_rules_group(); /** * If there is an at_rule and it already exists in the css_rules array, * add the rule to it. * Otherwise, create a new entry for the at_rule */ - if ( ! empty( $at_rule ) ) { - if ( isset( $this->css_rules[ "$at_rule $selector" ] ) ) { - $this->css_rules[ "$at_rule $selector" ]->add_declarations( $rule->get_declarations() ); + if ( ! empty( $rules_group ) ) { + if ( isset( $this->css_rules[ "$rules_group $selector" ] ) ) { + $this->css_rules[ "$rules_group $selector" ]->add_declarations( $rule->get_declarations() ); continue; } - $this->css_rules[ "$at_rule $selector" ] = $rule; + $this->css_rules[ "$rules_group $selector" ] = $rule; continue; } diff --git a/packages/style-engine/class-wp-style-engine.php b/packages/style-engine/class-wp-style-engine.php index a97e7cb8a0b213..23818eb889d015 100644 --- a/packages/style-engine/class-wp-style-engine.php +++ b/packages/style-engine/class-wp-style-engine.php @@ -355,14 +355,15 @@ protected static function is_valid_style_value( $style_value ) { * @param string $store_name A valid store key. * @param string $css_selector When a selector is passed, the function will return a full CSS rule `$selector { ...rules }`, otherwise a concatenated string of properties and values. * @param string[] $css_declarations An associative array of CSS definitions, e.g., array( "$property" => "$value", "$property" => "$value" ). + * @param string $rules_group Optional. A parent CSS selector in the case of nested CSS, or a CSS nested @rule, such as `@media (min-width: 80rem)` or `@layer module`. * * @return void. */ - public static function store_css_rule( $store_name, $css_selector, $css_declarations, $css_at_rule = '' ) { + public static function store_css_rule( $store_name, $css_selector, $css_declarations, $rules_group = '' ) { if ( empty( $store_name ) || empty( $css_selector ) || empty( $css_declarations ) ) { return; } - static::get_store( $store_name )->add_rule( $css_selector, $css_at_rule )->add_declarations( $css_declarations ); + static::get_store( $store_name )->add_rule( $css_selector, $rules_group )->add_declarations( $css_declarations ); } /** diff --git a/packages/style-engine/style-engine.php b/packages/style-engine/style-engine.php index 034236ffef1979..74df1b1040f6c7 100644 --- a/packages/style-engine/style-engine.php +++ b/packages/style-engine/style-engine.php @@ -83,7 +83,7 @@ function wp_style_engine_get_styles( $block_styles, $options = array() ) { * Required. A collection of CSS rules. * * @type array ...$0 { - * @type string $at_rule A CSS nested @rule, such as `@media (min-width: 80rem)` or `@layer module`. + * @type string $rules_group A parent CSS selector in the case of nested CSS, or a CSS nested @rule, such as `@media (min-width: 80rem)` or `@layer module`. * @type string $selector A CSS selector. * @type string[] $declarations An associative array of CSS definitions, e.g., array( "$property" => "$value", "$property" => "$value" ). * } @@ -117,13 +117,12 @@ function wp_style_engine_get_stylesheet_from_css_rules( $css_rules, $options = a continue; } - $at_rule = ! empty( $css_rule['at_rule'] ) ? $css_rule['at_rule'] : ''; - + $rules_group = $css_rule['rules_group'] ?? null; if ( ! empty( $options['context'] ) ) { - WP_Style_Engine::store_css_rule( $options['context'], $css_rule['selector'], $css_rule['declarations'], $at_rule ); + WP_Style_Engine::store_css_rule( $options['context'], $css_rule['selector'], $css_rule['declarations'], $rules_group ); } - $css_rule_objects[] = new WP_Style_Engine_CSS_Rule( $css_rule['selector'], $css_rule['declarations'], $at_rule ); + $css_rule_objects[] = new WP_Style_Engine_CSS_Rule( $css_rule['selector'], $css_rule['declarations'], $rules_group ); } if ( empty( $css_rule_objects ) ) { diff --git a/phpunit/style-engine/class-wp-style-engine-css-rule-test.php b/phpunit/style-engine/class-wp-style-engine-css-rule-test.php index 444c83f150f911..3a513ebb8b3bdd 100644 --- a/phpunit/style-engine/class-wp-style-engine-css-rule-test.php +++ b/phpunit/style-engine/class-wp-style-engine-css-rule-test.php @@ -34,6 +34,22 @@ public function test_should_instantiate_with_selector_and_rules() { $this->assertSame( $expected, $css_rule->get_css(), 'Value returned by get_css() does not match expected declarations string.' ); } + /** + * Tests setting and getting a rules group. + * + * @covers ::set_rules_group + * @covers ::get_rules_group + */ + public function test_should_set_rules_group() { + $rule = new WP_Style_Engine_CSS_Rule_Gutenberg( '.heres-johnny', array(), '@layer state' ); + + $this->assertSame( '@layer state', $rule->get_rules_group(), 'Return value of get_rules_group() does not match value passed to constructor.' ); + + $rule->set_rules_group( '@layer pony' ); + + $this->assertSame( '@layer pony', $rule->get_rules_group(), 'Return value of get_rules_group() does not match value passed to set_rules_group().' ); + } + /** * Tests that declaration properties are deduplicated. * diff --git a/phpunit/style-engine/class-wp-style-engine-css-rules-store-test.php b/phpunit/style-engine/class-wp-style-engine-css-rules-store-test.php index 8529bff78e22c8..297e05b5580d58 100644 --- a/phpunit/style-engine/class-wp-style-engine-css-rules-store-test.php +++ b/phpunit/style-engine/class-wp-style-engine-css-rules-store-test.php @@ -170,4 +170,20 @@ public function test_should_get_all_rule_objects_for_a_store() { $this->assertSame( $expected, $new_pizza_store->get_all_rules(), 'Return value for get_all_rules() does not match expectations after adding new rules to store.' ); } + + /** + * Tests adding rules group keys to store. + * + * @covers ::add_rule + */ + public function test_should_store_as_concatenated_rules_groups_and_selector() { + $store_one = WP_Style_Engine_CSS_Rules_Store_Gutenberg::get_store( 'one' ); + $store_one_rule = $store_one->add_rule( '.tony', '.one' ); + + $this->assertSame( + '.one .tony', + "{$store_one_rule->get_rules_group()} {$store_one_rule->get_selector()}", + 'add_rule() does not concatenate rules group and selector.' + ); + } } diff --git a/phpunit/style-engine/class-wp-style-engine-processor-test.php b/phpunit/style-engine/class-wp-style-engine-processor-test.php index a1ec32a20977c8..6d3a5a812e4fd3 100644 --- a/phpunit/style-engine/class-wp-style-engine-processor-test.php +++ b/phpunit/style-engine/class-wp-style-engine-processor-test.php @@ -58,7 +58,7 @@ public function test_should_return_nested_rules_as_compiled_css() { 'background-color' => 'purple', ) ); - $a_nice_css_rule->set_at_rule( '@media (min-width: 80rem)' ); + $a_nice_css_rule->set_rules_group( '@media (min-width: 80rem)' ); $a_nicer_css_rule = new WP_Style_Engine_CSS_Rule_Gutenberg( '.a-nicer-rule' ); $a_nicer_css_rule->add_declarations( @@ -68,7 +68,7 @@ public function test_should_return_nested_rules_as_compiled_css() { 'background-color' => 'purple', ) ); - $a_nicer_css_rule->set_at_rule( '@layer nicety' ); + $a_nicer_css_rule->set_rules_group( '@layer nicety' ); $a_nice_processor = new WP_Style_Engine_Processor_Gutenberg(); $a_nice_processor->add_rules( array( $a_nice_css_rule, $a_nicer_css_rule ) ); @@ -143,7 +143,7 @@ public function test_should_return_prettified_nested_css_rules() { 'background-color' => 'orange', ) ); - $a_wonderful_css_rule->set_at_rule( '@media (min-width: 80rem)' ); + $a_wonderful_css_rule->set_rules_group( '@media (min-width: 80rem)' ); $a_very_wonderful_css_rule = new WP_Style_Engine_CSS_Rule_Gutenberg( '.a-very_wonderful-rule' ); $a_very_wonderful_css_rule->add_declarations( @@ -152,7 +152,7 @@ public function test_should_return_prettified_nested_css_rules() { 'background-color' => 'orange', ) ); - $a_very_wonderful_css_rule->set_at_rule( '@layer wonderfulness' ); + $a_very_wonderful_css_rule->set_rules_group( '@layer wonderfulness' ); $a_wonderful_processor = new WP_Style_Engine_Processor_Gutenberg(); $a_wonderful_processor->add_rules( array( $a_wonderful_css_rule, $a_very_wonderful_css_rule ) ); diff --git a/phpunit/style-engine/style-engine-test.php b/phpunit/style-engine/style-engine-test.php index b66ccf5694bd66..63e5b3c1c7f7f0 100644 --- a/phpunit/style-engine/style-engine-test.php +++ b/phpunit/style-engine/style-engine-test.php @@ -23,7 +23,7 @@ public function tear_down() { /** * Tests generating block styles and classnames based on various manifestations of the $block_styles argument. * - * @covers ::gutenberg_style_engine_get_styles + * @covers ::wp_style_engine_get_styles * @covers WP_Style_Engine_Gutenberg::parse_block_styles * @covers WP_Style_Engine_Gutenberg::compile_css * @@ -531,7 +531,7 @@ public function data_wp_style_engine_get_styles() { /** * Tests adding rules to a store and retrieving a generated stylesheet. * - * @covers ::gutenberg_style_engine_get_styles + * @covers ::wp_style_engine_get_styles * @covers WP_Style_Engine_Gutenberg::store_css_rule */ public function test_should_store_block_styles_using_context() { @@ -666,7 +666,7 @@ public function test_should_return_stylesheet_from_css_rules() { * * @ticket 58811 * - * @covers ::gutenberg_style_engine_get_stylesheet_from_css_rules + * @covers ::wp_style_engine_get_stylesheet_from_css_rules * @covers WP_Style_Engine_Gutenberg::compile_stylesheet_from_css_rules */ public function test_should_dedupe_and_merge_css_rules() { @@ -716,7 +716,7 @@ public function test_should_dedupe_and_merge_css_rules() { * * This is testing this fix: https://github.com/WordPress/gutenberg/pull/49004 * - * @covers ::gutenberg_style_engine_get_stylesheet_from_css_rules + * @covers ::wp_style_engine_get_stylesheet_from_css_rules * @covers WP_Style_Engine_Gutenberg::compile_stylesheet_from_css_rules */ public function test_should_return_stylesheet_from_duotone_css_rules() { @@ -736,4 +736,60 @@ public function test_should_return_stylesheet_from_duotone_css_rules() { $compiled_stylesheet = gutenberg_style_engine_get_stylesheet_from_css_rules( $css_rules, array( 'prettify' => false ) ); $this->assertSame( ".wp-duotone-ffffff-000000-1{filter:url('#wp-duotone-ffffff-000000-1') !important;}", $compiled_stylesheet ); } + + /** + * Tests returning a generated stylesheet from a set of nested rules and merging their declarations. + */ + public function test_should_merge_declarations_for_rules_groups() { + $css_rules = array( + array( + 'selector' => '.saruman', + 'rules_group' => '@container (min-width: 700px)', + 'declarations' => array( + 'color' => 'white', + 'height' => '100px', + 'border-style' => 'solid', + 'align-self' => 'stretch', + ), + ), + array( + 'selector' => '.saruman', + 'rules_group' => '@container (min-width: 700px)', + 'declarations' => array( + 'color' => 'black', + 'font-family' => 'The-Great-Eye', + ), + ), + ); + + $compiled_stylesheet = gutenberg_style_engine_get_stylesheet_from_css_rules( $css_rules, array( 'prettify' => false ) ); + + $this->assertSame( '@container (min-width: 700px){.saruman{color:black;height:100px;border-style:solid;align-self:stretch;font-family:The-Great-Eye;}}', $compiled_stylesheet ); + } + + /** + * Tests returning a generated stylesheet from a set of nested rules. + */ + public function test_should_return_stylesheet_with_nested_rules() { + $css_rules = array( + array( + 'rules_group' => '.foo', + 'selector' => '@media (orientation: landscape)', + 'declarations' => array( + 'background-color' => 'blue', + ), + ), + array( + 'rules_group' => '.foo', + 'selector' => '@media (min-width > 1024px)', + 'declarations' => array( + 'background-color' => 'cotton-blue', + ), + ), + ); + + $compiled_stylesheet = gutenberg_style_engine_get_stylesheet_from_css_rules( $css_rules, array( 'prettify' => false ) ); + + $this->assertSame( '.foo{@media (orientation: landscape){background-color:blue;}}.foo{@media (min-width > 1024px){background-color:cotton-blue;}}', $compiled_stylesheet ); + } } From 8057df3b773373d73d849eec9e295ae5892d6a8f Mon Sep 17 00:00:00 2001 From: Nik Tsekouras Date: Tue, 13 Feb 2024 23:55:58 +0200 Subject: [PATCH 13/78] Patterns: Fix pattern categories on import (#58926) Co-authored-by: ntsekouras Co-authored-by: aaronrobertshaw Co-authored-by: glendaviesnz Co-authored-by: bph Co-authored-by: hanneslsm Co-authored-by: colorful-tones Co-authored-by: annezazu --- .../src/components/add-new-pattern/index.js | 38 +++++--- .../src/components/create-pattern-modal.js | 84 +---------------- packages/patterns/src/private-apis.js | 2 + packages/patterns/src/private-hooks.js | 91 +++++++++++++++++++ 4 files changed, 125 insertions(+), 90 deletions(-) create mode 100644 packages/patterns/src/private-hooks.js diff --git a/packages/edit-site/src/components/add-new-pattern/index.js b/packages/edit-site/src/components/add-new-pattern/index.js index 014ac6165aaef2..0d25ff67bf4482 100644 --- a/packages/edit-site/src/components/add-new-pattern/index.js +++ b/packages/edit-site/src/components/add-new-pattern/index.js @@ -25,10 +25,11 @@ import { PATTERN_DEFAULT_CATEGORY, TEMPLATE_PART_POST_TYPE, } from '../../utils/constants'; -import usePatternCategories from '../sidebar-navigation-screen-patterns/use-pattern-categories'; const { useHistory, useLocation } = unlock( routerPrivateApis ); -const { CreatePatternModal } = unlock( editPatternsPrivateApis ); +const { CreatePatternModal, useAddPatternCategory } = unlock( + editPatternsPrivateApis +); export default function AddNewPattern() { const history = useHistory(); @@ -43,7 +44,6 @@ export default function AddNewPattern() { const { createSuccessNotice, createErrorNotice } = useDispatch( noticesStore ); const patternUploadInputRef = useRef(); - const { patternCategories } = usePatternCategories(); function handleCreatePattern( { pattern, categoryId } ) { setShowPatternModal( false ); @@ -97,6 +97,7 @@ export default function AddNewPattern() { title: __( 'Import pattern from JSON' ), } ); + const { categoryMap, findOrCreateTerm } = useAddPatternCategory(); return ( <> - category.name === params.categoryId - )?.id; + let currentCategoryId; + // When we're not handling template parts, we should + // add or create the proper pattern category. + if ( params.categoryType !== TEMPLATE_PART_POST_TYPE ) { + const currentCategory = categoryMap + .values() + .find( + ( term ) => term.name === params.categoryId + ); + if ( !! currentCategory ) { + currentCategoryId = + currentCategory.id || + ( await findOrCreateTerm( + currentCategory.label + ) ); + } + } const pattern = await createPatternFromFile( file, currentCategoryId @@ -146,8 +158,12 @@ export default function AddNewPattern() { ); // Navigate to the All patterns category for the newly created pattern - // if we're not on that page already. - if ( ! currentCategoryId ) { + // if we're not on that page already and if we're not in the `my-patterns` + // category. + if ( + ! currentCategoryId && + params.categoryId !== 'my-patterns' + ) { history.push( { path: `/patterns`, categoryType: PATTERN_TYPES.theme, diff --git a/packages/patterns/src/components/create-pattern-modal.js b/packages/patterns/src/components/create-pattern-modal.js index 137c14222ced34..9576e50309e237 100644 --- a/packages/patterns/src/components/create-pattern-modal.js +++ b/packages/patterns/src/components/create-pattern-modal.js @@ -10,21 +10,17 @@ import { ToggleControl, } from '@wordpress/components'; import { __, _x } from '@wordpress/i18n'; -import { useState, useMemo } from '@wordpress/element'; -import { useDispatch, useSelect } from '@wordpress/data'; +import { useState } from '@wordpress/element'; +import { useDispatch } from '@wordpress/data'; import { store as noticesStore } from '@wordpress/notices'; -import { store as coreStore } from '@wordpress/core-data'; /** * Internal dependencies */ import { PATTERN_DEFAULT_CATEGORY, PATTERN_SYNC_TYPES } from '../constants'; - -/** - * Internal dependencies - */ import { store as patternsStore } from '../store'; -import CategorySelector, { CATEGORY_SLUG } from './category-selector'; +import CategorySelector from './category-selector'; +import { useAddPatternCategory } from '../private-hooks'; import { unlock } from '../lock-unlock'; export default function CreatePatternModal( { @@ -59,47 +55,9 @@ export function CreatePatternModalContents( { const [ isSaving, setIsSaving ] = useState( false ); const { createPattern } = unlock( useDispatch( patternsStore ) ); - const { saveEntityRecord, invalidateResolution } = useDispatch( coreStore ); const { createErrorNotice } = useDispatch( noticesStore ); - const { corePatternCategories, userPatternCategories } = useSelect( - ( select ) => { - const { getUserPatternCategories, getBlockPatternCategories } = - select( coreStore ); - - return { - corePatternCategories: getBlockPatternCategories(), - userPatternCategories: getUserPatternCategories(), - }; - } - ); - - const categoryMap = useMemo( () => { - // Merge the user and core pattern categories and remove any duplicates. - const uniqueCategories = new Map(); - userPatternCategories.forEach( ( category ) => { - uniqueCategories.set( category.label.toLowerCase(), { - label: category.label, - name: category.name, - id: category.id, - } ); - } ); - - corePatternCategories.forEach( ( category ) => { - if ( - ! uniqueCategories.has( category.label.toLowerCase() ) && - // There are two core categories with `Post` label so explicitly remove the one with - // the `query` slug to avoid any confusion. - category.name !== 'query' - ) { - uniqueCategories.set( category.label.toLowerCase(), { - label: category.label, - name: category.name, - } ); - } - } ); - return uniqueCategories; - }, [ userPatternCategories, corePatternCategories ] ); + const { categoryMap, findOrCreateTerm } = useAddPatternCategory(); async function onCreate( patternTitle, sync ) { if ( ! title || isSaving ) { @@ -137,38 +95,6 @@ export function CreatePatternModalContents( { } } - /** - * @param {string} term - * @return {Promise} The pattern category id. - */ - async function findOrCreateTerm( term ) { - try { - const existingTerm = categoryMap.get( term.toLowerCase() ); - if ( existingTerm && existingTerm.id ) { - return existingTerm.id; - } - // If we have an existing core category we need to match the new user category to the - // correct slug rather than autogenerating it to prevent duplicates, eg. the core `Headers` - // category uses the singular `header` as the slug. - const termData = existingTerm - ? { name: existingTerm.label, slug: existingTerm.name } - : { name: term }; - const newTerm = await saveEntityRecord( - 'taxonomy', - CATEGORY_SLUG, - termData, - { throwOnError: true } - ); - invalidateResolution( 'getUserPatternCategories' ); - return newTerm.id; - } catch ( error ) { - if ( error.code !== 'term_exists' ) { - throw error; - } - - return error.data.term_id; - } - } return (
    { diff --git a/packages/patterns/src/private-apis.js b/packages/patterns/src/private-apis.js index 099e4ae8ffed4c..a5fbddb62fd62c 100644 --- a/packages/patterns/src/private-apis.js +++ b/packages/patterns/src/private-apis.js @@ -15,6 +15,7 @@ import PatternsMenuItems from './components'; import RenamePatternCategoryModal from './components/rename-pattern-category-modal'; import PartialSyncingControls from './components/partial-syncing-controls'; import ResetOverridesControl from './components/reset-overrides-control'; +import { useAddPatternCategory } from './private-hooks'; import { PATTERN_TYPES, PATTERN_DEFAULT_CATEGORY, @@ -35,6 +36,7 @@ lock( privateApis, { RenamePatternCategoryModal, PartialSyncingControls, ResetOverridesControl, + useAddPatternCategory, PATTERN_TYPES, PATTERN_DEFAULT_CATEGORY, PATTERN_USER_CATEGORY, diff --git a/packages/patterns/src/private-hooks.js b/packages/patterns/src/private-hooks.js new file mode 100644 index 00000000000000..7dee37222fbbda --- /dev/null +++ b/packages/patterns/src/private-hooks.js @@ -0,0 +1,91 @@ +/** + * WordPress dependencies + */ +import { useSelect, useDispatch } from '@wordpress/data'; +import { store as coreStore } from '@wordpress/core-data'; +import { useMemo } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { CATEGORY_SLUG } from './components/category-selector'; + +/** + * Helper hook that creates a Map with the core and user patterns categories + * and removes any duplicates. It's used when we need to create new user + * categories when creating or importing patterns. + * This hook also provides a function to find or create a pattern category. + * + * @return {Object} The merged categories map and the callback function to find or create a category. + */ +export function useAddPatternCategory() { + const { saveEntityRecord, invalidateResolution } = useDispatch( coreStore ); + const { corePatternCategories, userPatternCategories } = useSelect( + ( select ) => { + const { getUserPatternCategories, getBlockPatternCategories } = + select( coreStore ); + + return { + corePatternCategories: getBlockPatternCategories(), + userPatternCategories: getUserPatternCategories(), + }; + }, + [] + ); + const categoryMap = useMemo( () => { + // Merge the user and core pattern categories and remove any duplicates. + const uniqueCategories = new Map(); + userPatternCategories.forEach( ( category ) => { + uniqueCategories.set( category.label.toLowerCase(), { + label: category.label, + name: category.name, + id: category.id, + } ); + } ); + + corePatternCategories.forEach( ( category ) => { + if ( + ! uniqueCategories.has( category.label.toLowerCase() ) && + // There are two core categories with `Post` label so explicitly remove the one with + // the `query` slug to avoid any confusion. + category.name !== 'query' + ) { + uniqueCategories.set( category.label.toLowerCase(), { + label: category.label, + name: category.name, + } ); + } + } ); + return uniqueCategories; + }, [ userPatternCategories, corePatternCategories ] ); + + async function findOrCreateTerm( term ) { + try { + const existingTerm = categoryMap.get( term.toLowerCase() ); + if ( existingTerm?.id ) { + return existingTerm.id; + } + // If we have an existing core category we need to match the new user category to the + // correct slug rather than autogenerating it to prevent duplicates, eg. the core `Headers` + // category uses the singular `header` as the slug. + const termData = existingTerm + ? { name: existingTerm.label, slug: existingTerm.name } + : { name: term }; + const newTerm = await saveEntityRecord( + 'taxonomy', + CATEGORY_SLUG, + termData, + { throwOnError: true } + ); + invalidateResolution( 'getUserPatternCategories' ); + return newTerm.id; + } catch ( error ) { + if ( error.code !== 'term_exists' ) { + throw error; + } + return error.data.term_id; + } + } + + return { categoryMap, findOrCreateTerm }; +} From b35bc15f13e2287befaa638a3a6aa168c31b5362 Mon Sep 17 00:00:00 2001 From: Ben Dwyer Date: Tue, 13 Feb 2024 22:14:12 +0000 Subject: [PATCH 14/78] Shadows: Don't assume that core provides defaults (#58973) Co-authored-by: vcanales Co-authored-by: scruffian --- .../src/components/global-styles/shadow-panel-components.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/block-editor/src/components/global-styles/shadow-panel-components.js b/packages/block-editor/src/components/global-styles/shadow-panel-components.js index 6e4e3a15b184d8..8c9ba795bc17ba 100644 --- a/packages/block-editor/src/components/global-styles/shadow-panel-components.js +++ b/packages/block-editor/src/components/global-styles/shadow-panel-components.js @@ -19,13 +19,13 @@ import { shadow as shadowIcon, Icon, check } from '@wordpress/icons'; import classNames from 'classnames'; export function ShadowPopoverContainer( { shadow, onShadowChange, settings } ) { - const defaultShadows = settings?.shadow?.presets?.default; - const themeShadows = settings?.shadow?.presets?.theme; + const defaultShadows = settings?.shadow?.presets?.default || []; + const themeShadows = settings?.shadow?.presets?.theme || []; const defaultPresetsEnabled = settings?.shadow?.defaultPresets; const shadows = [ ...( defaultPresetsEnabled ? defaultShadows : [] ), - ...( themeShadows || [] ), + ...themeShadows, ]; return ( From 24fcc49d1a1c553d4970a61a9f4ab280fe71eda1 Mon Sep 17 00:00:00 2001 From: Robert Anderson Date: Wed, 14 Feb 2024 10:27:40 +1100 Subject: [PATCH 15/78] Remove @noisysocks from CODEOWNERS --- .github/CODEOWNERS | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index bdd97598763621..fda634868414e0 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -49,9 +49,9 @@ /packages/block-editor/src/components/link-control @getdave # Widgets -/packages/edit-widgets @draganescu @talldan @noisysocks @tellthemachines @adamziel @kevin940726 -/packages/customize-widgets @noisysocks -/packages/widgets @noisysocks +/packages/edit-widgets @draganescu @talldan @tellthemachines @adamziel @kevin940726 +/packages/customize-widgets +/packages/widgets # Full Site Editing /packages/edit-site From 73bd8047cdf19a81790cd5332cde9512021414f6 Mon Sep 17 00:00:00 2001 From: tellthemachines Date: Wed, 14 Feb 2024 14:02:52 +1100 Subject: [PATCH 16/78] Update codeowners for tellthemachines --- .github/CODEOWNERS | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index fda634868414e0..a54091e4745989 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -15,10 +15,6 @@ # Blocks /packages/block-library @ajitbohra /packages/block-library/src/gallery @geriux -/packages/block-library/src/navigation @tellthemachines -/packages/block-library/src/navigation-link @tellthemachines -/packages/block-library/src/navigation-submenu @tellthemachines -/packages/block-library/src/page-list @tellthemachines /packages/block-library/src/comment-template @michalczaplinski /packages/block-library/src/comments @michalczaplinski /packages/block-library/src/table-of-contents @ZebulanStanphill @@ -34,6 +30,8 @@ /packages/annotations @atimmer /packages/autop /packages/block-editor @ellatrix +/packages/block-editor/src/hooks @tellthemachines +/packages/block-editor/src/layouts @tellthemachines /packages/block-serialization-spec-parser @dmsnell /packages/block-serialization-default-parser @dmsnell /packages/blocks @@ -49,7 +47,7 @@ /packages/block-editor/src/components/link-control @getdave # Widgets -/packages/edit-widgets @draganescu @talldan @tellthemachines @adamziel @kevin940726 +/packages/edit-widgets @draganescu @talldan @adamziel @kevin940726 /packages/customize-widgets /packages/widgets @@ -136,6 +134,8 @@ # PHP /lib @spacedmonkey +/lib/block-supports/layout.php @tellthemachines +/lib/class-wp-theme-json-gutenberg.php @tellthemachines /lib/compat/*/html-api @dmsnell /lib/experimental/rest-api.php @timothybjacobs /lib/experimental/class-wp-rest-* @timothybjacobs From 39b659dafc0332f475a3c5c22429edf77e10d9c5 Mon Sep 17 00:00:00 2001 From: George Mamadashvili Date: Wed, 14 Feb 2024 07:17:03 +0400 Subject: [PATCH 17/78] Inserter: Don't select the closest block with editing mode set as disabled (#58971) Co-authored-by: Mamaduka Co-authored-by: noisysocks Co-authored-by: dernin --- .../src/components/block-tools/insertion-point.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/block-editor/src/components/block-tools/insertion-point.js b/packages/block-editor/src/components/block-tools/insertion-point.js index 19ad39caca336a..bd4c8ea5fb371c 100644 --- a/packages/block-editor/src/components/block-tools/insertion-point.js +++ b/packages/block-editor/src/components/block-tools/insertion-point.js @@ -81,11 +81,16 @@ function InbetweenInsertionPointPopover( { isInserterShown: insertionPoint?.__unstableWithInserter, }; }, [] ); + const { getBlockEditingMode } = useSelect( blockEditorStore ); const disableMotion = useReducedMotion(); function onClick( event ) { - if ( event.target === ref.current && nextClientId ) { + if ( + event.target === ref.current && + nextClientId && + getBlockEditingMode( nextClientId ) !== 'disabled' + ) { selectBlock( nextClientId, -1 ); } } From 4bf1d217f631af93031862989703122f2f474545 Mon Sep 17 00:00:00 2001 From: tellthemachines Date: Wed, 14 Feb 2024 15:52:03 +1100 Subject: [PATCH 18/78] Add support for column and row span in grid children. (#58539) Co-authored-by: tellthemachines Co-authored-by: ramonjd Co-authored-by: andrewserong Co-authored-by: noisysocks Co-authored-by: SaxonF Co-authored-by: aristath Co-authored-by: fabiankaegy --- lib/block-supports/layout.php | 110 ++++++++++---- lib/experimental/kses.php | 14 ++ .../components/child-layout-control/index.js | 137 ++++++++++++------ .../global-styles/dimensions-panel.js | 58 +++++--- .../block-editor/src/hooks/layout-child.js | 51 ++++++- packages/block-editor/src/layouts/grid.js | 42 ++---- .../block-editor/src/layouts/test/grid.js | 18 ++- phpunit/block-supports/layout-test.php | 19 +++ 8 files changed, 318 insertions(+), 131 deletions(-) diff --git a/lib/block-supports/layout.php b/lib/block-supports/layout.php index 07ccb6089721ea..3c7117ca26a1f5 100644 --- a/lib/block-supports/layout.php +++ b/lib/block-supports/layout.php @@ -476,7 +476,10 @@ function gutenberg_get_layout_style( $selector, $layout, $has_block_gap_support $layout_styles[] = array( 'selector' => $selector, - 'declarations' => array( 'grid-template-columns' => 'repeat(auto-fill, minmax(min(' . $minimum_column_width . ', 100%), 1fr))' ), + 'declarations' => array( + 'grid-template-columns' => 'repeat(auto-fill, minmax(min(' . $minimum_column_width . ', 100%), 1fr))', + 'container-type' => 'inline-size', + ), ); } @@ -557,44 +560,95 @@ function gutenberg_incremental_id_per_prefix( $prefix = '' ) { function gutenberg_render_layout_support_flag( $block_content, $block ) { $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block['blockName'] ); $block_supports_layout = block_has_support( $block_type, array( 'layout' ), false ) || block_has_support( $block_type, array( '__experimentalLayout' ), false ); - $layout_from_parent = $block['attrs']['style']['layout']['selfStretch'] ?? null; + // If there is any value in style -> layout, the block has a child layout. + $child_layout = $block['attrs']['style']['layout'] ?? null; - if ( ! $block_supports_layout && ! $layout_from_parent ) { + if ( ! $block_supports_layout && ! $child_layout ) { return $block_content; } - $outer_class_names = array(); + $outer_class_names = array(); + $container_content_class = wp_unique_id( 'wp-container-content-' ); + $child_layout_declarations = array(); + $child_layout_styles = array(); - if ( 'fixed' === $layout_from_parent || 'fill' === $layout_from_parent ) { - $container_content_class = wp_unique_id( 'wp-container-content-' ); + $self_stretch = isset( $block['attrs']['style']['layout']['selfStretch'] ) ? $block['attrs']['style']['layout']['selfStretch'] : null; - $child_layout_styles = array(); + if ( 'fixed' === $self_stretch && isset( $block['attrs']['style']['layout']['flexSize'] ) ) { + $child_layout_declarations['flex-basis'] = $block['attrs']['style']['layout']['flexSize']; + $child_layout_declarations['box-sizing'] = 'border-box'; + } elseif ( 'fill' === $self_stretch ) { + $child_layout_declarations['flex-grow'] = '1'; + } - if ( 'fixed' === $layout_from_parent && isset( $block['attrs']['style']['layout']['flexSize'] ) ) { - $child_layout_styles[] = array( - 'selector' => ".$container_content_class", - 'declarations' => array( - 'flex-basis' => $block['attrs']['style']['layout']['flexSize'], - 'box-sizing' => 'border-box', - ), - ); - } elseif ( 'fill' === $layout_from_parent ) { - $child_layout_styles[] = array( - 'selector' => ".$container_content_class", - 'declarations' => array( - 'flex-grow' => '1', - ), - ); + if ( isset( $block['attrs']['style']['layout']['columnSpan'] ) ) { + $column_span = $block['attrs']['style']['layout']['columnSpan']; + $child_layout_declarations['grid-column'] = "span $column_span"; + } + if ( isset( $block['attrs']['style']['layout']['rowSpan'] ) ) { + $row_span = $block['attrs']['style']['layout']['rowSpan']; + $child_layout_declarations['grid-row'] = "span $row_span"; + } + $child_layout_styles[] = array( + 'selector' => ".$container_content_class", + 'declarations' => $child_layout_declarations, + ); + + /** + * If columnSpan is set, and the parent grid is responsive, i.e. if it has a minimumColumnWidth set, + * the columnSpan should be removed on small grids. + */ + if ( isset( $block['attrs']['style']['layout']['columnSpan'] ) && isset( $block['attrs']['style']['layout']['parentColumnWidth'] ) ) { + $column_span_number = floatval( $block['attrs']['style']['layout']['columnSpan'] ); + $parent_column_width = $block['attrs']['style']['layout']['parentColumnWidth']; + $parent_column_value = floatval( $parent_column_width ); + $parent_column_unit = explode( $parent_column_value, $parent_column_width ); + /** + * If there is no unit, the width has somehow been mangled so we reset both unit and value + * to defaults. + * Additionally, the unit should be one of px, rem or em, so that also needs to be checked. + */ + if ( count( $parent_column_unit ) <= 1 ) { + $parent_column_unit = 'rem'; + $parent_column_value = 12; + } else { + $parent_column_unit = $parent_column_unit[1]; + + if ( ! in_array( $parent_column_unit, array( 'px', 'rem', 'em' ), true ) ) { + $parent_column_unit = 'rem'; + } } - gutenberg_style_engine_get_stylesheet_from_css_rules( - $child_layout_styles, - array( - 'context' => 'block-supports', - 'prettify' => false, - ) + /** + * A default gap value is used for this computation because custom gap values may not be + * viable to use in the computation of the container query value. + */ + $default_gap_value = 'px' === $parent_column_unit ? 24 : 1.5; + $container_query_value = $column_span_number * $parent_column_value + ( $column_span_number - 1 ) * $default_gap_value; + $container_query_value = $container_query_value . $parent_column_unit; + + $child_layout_styles[] = array( + 'rules_group' => "@container (max-width: $container_query_value )", + 'selector' => ".$container_content_class", + 'declarations' => array( + 'grid-column' => '1/-1', + ), ); + } + + /** + * Add to the style engine store to enqueue and render layout styles. + * Return styles here just to check if any exist. + */ + $child_css = gutenberg_style_engine_get_stylesheet_from_css_rules( + $child_layout_styles, + array( + 'context' => 'block-supports', + 'prettify' => false, + ) + ); + if ( $child_css ) { $outer_class_names[] = $container_content_class; } diff --git a/lib/experimental/kses.php b/lib/experimental/kses.php index d447b10fcbd05d..bed5c58697e626 100644 --- a/lib/experimental/kses.php +++ b/lib/experimental/kses.php @@ -87,3 +87,17 @@ function allow_filter_in_styles( $allow_css, $css_test_string ) { } } add_filter( 'safecss_filter_attr_allow_css', 'allow_filter_in_styles', 10, 2 ); + +/** + * Update allowed inline style attributes list. + * + * @param string[] $attrs Array of allowed CSS attributes. + * @return string[] CSS attributes. + */ +function gutenberg_safe_grid_attrs( $attrs ) { + $attrs[] = 'grid-column'; + $attrs[] = 'grid-row'; + $attrs[] = 'container-type'; + return $attrs; +} +add_filter( 'safe_style_css', 'gutenberg_safe_grid_attrs' ); diff --git a/packages/block-editor/src/components/child-layout-control/index.js b/packages/block-editor/src/components/child-layout-control/index.js index 10dffa0eeed8de..59ec27dcfbc6f9 100644 --- a/packages/block-editor/src/components/child-layout-control/index.js +++ b/packages/block-editor/src/components/child-layout-control/index.js @@ -5,6 +5,8 @@ import { __experimentalToggleGroupControl as ToggleGroupControl, __experimentalToggleGroupControlOption as ToggleGroupControlOption, __experimentalUnitControl as UnitControl, + __experimentalInputControl as InputControl, + __experimentalHStack as HStack, } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { useEffect } from '@wordpress/element'; @@ -38,7 +40,18 @@ export default function ChildLayoutControl( { onChange, parentLayout, } ) { - const { selfStretch, flexSize } = childLayout; + const { selfStretch, flexSize, columnSpan, rowSpan } = childLayout; + const { + type: parentLayoutType, + minimumColumnWidth = '12rem', + columnCount, + } = parentLayout; + + /** + * If columnCount is set, the parentColumnwidth isn't needed because + * the grid has a fixed number of columns with responsive widths. + */ + const parentColumnWidth = columnCount ? null : minimumColumnWidth; useEffect( () => { if ( selfStretch === 'fixed' && ! flexSize ) { @@ -51,49 +64,85 @@ export default function ChildLayoutControl( { return ( <> - { - const newFlexSize = value !== 'fixed' ? null : flexSize; - onChange( { - ...childLayout, - selfStretch: value, - flexSize: newFlexSize, - } ); - } } - isBlock={ true } - > - - - - - { selfStretch === 'fixed' && ( - { - onChange( { - ...childLayout, - flexSize: value, - } ); - } } - value={ flexSize } - /> + { parentLayoutType === 'flex' && ( + <> + { + const newFlexSize = + value !== 'fixed' ? null : flexSize; + onChange( { + selfStretch: value, + flexSize: newFlexSize, + } ); + } } + isBlock={ true } + > + + + + + { selfStretch === 'fixed' && ( + { + onChange( { + selfStretch, + flexSize: value, + } ); + } } + value={ flexSize } + /> + ) } + + ) } + { parentLayoutType === 'grid' && ( + + { + onChange( { + rowSpan, + columnSpan: value, + parentColumnWidth, + } ); + } } + value={ columnSpan } + min={ 1 } + /> + { + onChange( { + columnSpan, + parentColumnWidth, + rowSpan: value, + } ); + } } + value={ rowSpan } + min={ 1 } + /> + ) } ); diff --git a/packages/block-editor/src/components/global-styles/dimensions-panel.js b/packages/block-editor/src/components/global-styles/dimensions-panel.js index 0d486e29452637..c3b60e8f65d607 100644 --- a/packages/block-editor/src/components/global-styles/dimensions-panel.js +++ b/packages/block-editor/src/components/global-styles/dimensions-panel.js @@ -92,7 +92,10 @@ function useHasChildLayout( settings ) { } = settings?.parentLayout ?? {}; const support = - ( defaultParentLayoutType === 'flex' || parentLayoutType === 'flex' ) && + ( defaultParentLayoutType === 'flex' || + parentLayoutType === 'flex' || + defaultParentLayoutType === 'grid' || + parentLayoutType === 'grid' ) && allowSizingOnChildren; return !! settings?.layout && support; @@ -394,14 +397,17 @@ export default function DimensionsPanel( { // Child Layout const showChildLayoutControl = useHasChildLayout( settings ); const childLayout = inheritedValue?.layout; + const { selfStretch } = childLayout ?? {}; const { orientation = 'horizontal' } = settings?.parentLayout ?? {}; - const childLayoutOrientationLabel = + const flexResetLabel = orientation === 'horizontal' ? __( 'Width' ) : __( 'Height' ); + const childLayoutResetLabel = selfStretch + ? flexResetLabel + : __( 'Grid spans' ); const setChildLayout = ( newChildLayout ) => { onChange( { ...value, layout: { - ...value?.layout, ...newChildLayout, }, } ); @@ -410,6 +416,9 @@ export default function DimensionsPanel( { setChildLayout( { selfStretch: undefined, flexSize: undefined, + columnSpan: undefined, + rowSpan: undefined, + parentColumnWidth: undefined, } ); }; const hasChildLayoutValue = () => !! value?.layout; @@ -423,6 +432,9 @@ export default function DimensionsPanel( { wideSize: undefined, selfStretch: undefined, flexSize: undefined, + columnSpan: undefined, + rowSpan: undefined, + parentColumnWidth: undefined, } ), spacing: { ...previousValue?.spacing, @@ -637,6 +649,26 @@ export default function DimensionsPanel( { ) } ) } + { showChildLayoutControl && ( + + + + ) } { showMinHeightControl && ( ) } - { showChildLayoutControl && ( - - - - ) } ); } diff --git a/packages/block-editor/src/hooks/layout-child.js b/packages/block-editor/src/hooks/layout-child.js index 58a75a568c40d0..d3219d8dfc5636 100644 --- a/packages/block-editor/src/hooks/layout-child.js +++ b/packages/block-editor/src/hooks/layout-child.js @@ -15,7 +15,8 @@ function useBlockPropsChildLayoutStyles( { style } ) { return ! select( blockEditorStore ).getSettings().disableLayoutStyles; } ); const layout = style?.layout ?? {}; - const { selfStretch, flexSize } = layout; + const { selfStretch, flexSize, columnSpan, rowSpan, parentColumnWidth } = + layout; const id = useInstanceId( useBlockPropsChildLayoutStyles ); const selector = `.wp-container-content-${ id }`; @@ -30,6 +31,54 @@ function useBlockPropsChildLayoutStyles( { style } ) { css = `${ selector } { flex-grow: 1; }`; + } else if ( columnSpan ) { + css = `${ selector } { + grid-column: span ${ columnSpan }; + }`; + } + /** + * If parentColumnWidth is set, the grid is responsive + * so a container query is needed for the span to resize. + */ + if ( columnSpan && parentColumnWidth ) { + // Calculate the container query value. + const columnSpanNumber = parseInt( columnSpan ); + let parentColumnValue = parseFloat( parentColumnWidth ); + /** + * 12rem is the default minimumColumnWidth value. + * If parentColumnValue is not a number, default to 12. + */ + if ( isNaN( parentColumnValue ) ) { + parentColumnValue = 12; + } + + let parentColumnUnit = parentColumnWidth?.replace( + parentColumnValue, + '' + ); + /** + * Check that parent column unit is either 'px', 'rem' or 'em'. + * If not, default to 'rem'. + */ + if ( ! [ 'px', 'rem', 'em' ].includes( parentColumnUnit ) ) { + parentColumnUnit = 'rem'; + } + + const defaultGapValue = parentColumnUnit === 'px' ? 24 : 1.5; + const containerQueryValue = + columnSpanNumber * parentColumnValue + + ( columnSpanNumber - 1 ) * defaultGapValue; + + css += `@container (max-width: ${ containerQueryValue }${ parentColumnUnit }) { + ${ selector } { + grid-column: 1 / -1; + } + }`; + } + if ( rowSpan ) { + css += `${ selector } { + grid-row: span ${ rowSpan }; + }`; } } diff --git a/packages/block-editor/src/layouts/grid.js b/packages/block-editor/src/layouts/grid.js index 6dd22cf87ba599..790998472fd26e 100644 --- a/packages/block-editor/src/layouts/grid.js +++ b/packages/block-editor/src/layouts/grid.js @@ -51,6 +51,12 @@ const RANGE_CONTROL_MAX_VALUES = { dvmax: 100, }; +const units = [ + { value: 'px', label: 'px', default: 0 }, + { value: 'rem', label: 'rem', default: 0 }, + { value: 'em', label: 'em', default: 0 }, +]; + export default { name: 'grid', label: __( 'Grid' ), @@ -97,7 +103,8 @@ export default { ); } else if ( minimumColumnWidth ) { rules.push( - `grid-template-columns: repeat(auto-fill, minmax(min(${ minimumColumnWidth }, 100%), 1fr))` + `grid-template-columns: repeat(auto-fill, minmax(min(${ minimumColumnWidth }, 100%), 1fr))`, + `container-type: inline-size` ); } @@ -152,38 +159,6 @@ function GridLayoutMinimumWidthControl( { layout, onChange } ) { } else if ( [ 'em', 'rem' ].includes( unit ) && newUnit === 'px' ) { // Convert to pixel value assuming a root size of 16px. newValue = Math.round( quantity * 16 ) + newUnit; - } else if ( - [ - 'vh', - 'vw', - '%', - 'svw', - 'lvw', - 'dvw', - 'svh', - 'lvh', - 'dvh', - 'vi', - 'svi', - 'lvi', - 'dvi', - 'vb', - 'svb', - 'lvb', - 'dvb', - 'vmin', - 'svmin', - 'lvmin', - 'dvmin', - 'vmax', - 'svmax', - 'lvmax', - 'dvmax', - ].includes( newUnit ) && - quantity > 100 - ) { - // When converting to `%` or viewport-relative units, cap the new value at 100. - newValue = 100 + newUnit; } onChange( { @@ -209,6 +184,7 @@ function GridLayoutMinimumWidthControl( { layout, onChange } ) { } } onUnitChange={ handleUnitChange } value={ value } + units={ units } min={ 0 } /> diff --git a/packages/block-editor/src/layouts/test/grid.js b/packages/block-editor/src/layouts/test/grid.js index 634457670f0dfa..79257ec0200be1 100644 --- a/packages/block-editor/src/layouts/test/grid.js +++ b/packages/block-editor/src/layouts/test/grid.js @@ -4,8 +4,8 @@ import grid from '../grid'; describe( 'getLayoutStyle', () => { - it( 'should return a single `grid-template-columns` property if no non-default params are provided', () => { - const expected = `.editor-styles-wrapper .my-container { grid-template-columns: repeat(auto-fill, minmax(min(12rem, 100%), 1fr)); }`; + it( 'should return only `grid-template-columns` and `container-type` properties if no non-default params are provided', () => { + const expected = `.editor-styles-wrapper .my-container { grid-template-columns: repeat(auto-fill, minmax(min(12rem, 100%), 1fr)); container-type: inline-size; }`; const result = grid.getLayoutStyle( { selector: '.my-container', @@ -16,6 +16,20 @@ describe( 'getLayoutStyle', () => { layoutDefinitions: undefined, } ); + expect( result ).toBe( expected ); + } ); + it( 'should return only `grid-template-columns` if columnCount property is provided', () => { + const expected = `.editor-styles-wrapper .my-container { grid-template-columns: repeat(3, minmax(0, 1fr)); }`; + + const result = grid.getLayoutStyle( { + selector: '.my-container', + layout: { columnCount: 3 }, + style: {}, + blockName: 'test-block', + hasBlockGapSupport: false, + layoutDefinitions: undefined, + } ); + expect( result ).toBe( expected ); } ); } ); diff --git a/phpunit/block-supports/layout-test.php b/phpunit/block-supports/layout-test.php index 41735fdc0939e2..3e53d8b05ab811 100644 --- a/phpunit/block-supports/layout-test.php +++ b/phpunit/block-supports/layout-test.php @@ -364,6 +364,25 @@ public function data_gutenberg_get_layout_style() { ), 'expected_output' => '.wp-layout{flex-wrap:nowrap;flex-direction:column;align-items:flex-start;justify-content:flex-end;}', ), + 'default grid layout' => array( + 'args' => array( + 'selector' => '.wp-layout', + 'layout' => array( + 'type' => 'grid', + ), + ), + 'expected_output' => '.wp-layout{grid-template-columns:repeat(auto-fill, minmax(min(12rem, 100%), 1fr));container-type:inline-size;}', + ), + 'grid layout with columnCount' => array( + 'args' => array( + 'selector' => '.wp-layout', + 'layout' => array( + 'type' => 'grid', + 'columnCount' => 3, + ), + ), + 'expected_output' => '.wp-layout{grid-template-columns:repeat(3, minmax(0, 1fr));}', + ), 'default layout with blockGap to verify converting gap value into valid CSS' => array( 'args' => array( 'selector' => '.wp-block-group.wp-container-6', From 0e2762c82e2bce0c3b9a895e433021f8d2059b15 Mon Sep 17 00:00:00 2001 From: Andrea Fercia Date: Wed, 14 Feb 2024 09:48:53 +0100 Subject: [PATCH 19/78] Make the custom CSS validation error message accessible. (#56690) Co-authored-by: Alex Stine --- .../components/global-styles/advanced-panel.js | 18 ++++++------------ .../src/components/global-styles/style.scss | 10 ---------- 2 files changed, 6 insertions(+), 22 deletions(-) diff --git a/packages/block-editor/src/components/global-styles/advanced-panel.js b/packages/block-editor/src/components/global-styles/advanced-panel.js index af43552c0a3eba..1ad59451d1468a 100644 --- a/packages/block-editor/src/components/global-styles/advanced-panel.js +++ b/packages/block-editor/src/components/global-styles/advanced-panel.js @@ -3,12 +3,11 @@ */ import { TextareaControl, - Tooltip, + Notice, __experimentalVStack as VStack, } from '@wordpress/components'; import { useState } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; -import { Icon, info } from '@wordpress/icons'; /** * Internal dependencies @@ -58,6 +57,11 @@ export default function AdvancedPanel( { return ( + { cssError && ( + setCSSError( null ) }> + { cssError } + + ) } - { cssError && ( - -
    - -
    -
    - ) }
    ); } diff --git a/packages/block-editor/src/components/global-styles/style.scss b/packages/block-editor/src/components/global-styles/style.scss index 010c5faaefff44..d2ba88f9f31e00 100644 --- a/packages/block-editor/src/components/global-styles/style.scss +++ b/packages/block-editor/src/components/global-styles/style.scss @@ -47,13 +47,3 @@ /*rtl:ignore*/ direction: ltr; } - -.block-editor-global-styles-advanced-panel__custom-css-validation-wrapper { - position: absolute; - bottom: $grid-unit-20; - right: $grid-unit * 3; -} - -.block-editor-global-styles-advanced-panel__custom-css-validation-icon { - fill: $alert-red; -} From 8cc54eeb1c37cc1c41274b27b8a9145e1955ff3d Mon Sep 17 00:00:00 2001 From: Andrea Fercia Date: Wed, 14 Feb 2024 10:06:10 +0100 Subject: [PATCH 20/78] Remove menubar role and improve complementary area header semantics. (#58740) Co-authored-by: afercia Co-authored-by: alexstine --- .../sidebar-edit-mode/global-styles-sidebar.js | 10 ++++------ .../src/components/sidebar-edit-mode/style.scss | 4 ++++ .../src/components/complementary-area-header/index.js | 4 ++-- .../components/complementary-area-header/style.scss | 5 +++++ .../src/components/complementary-area/index.js | 4 +++- 5 files changed, 18 insertions(+), 9 deletions(-) diff --git a/packages/edit-site/src/components/sidebar-edit-mode/global-styles-sidebar.js b/packages/edit-site/src/components/sidebar-edit-mode/global-styles-sidebar.js index 25d10c51645fc2..14d1d10691c252 100644 --- a/packages/edit-site/src/components/sidebar-edit-mode/global-styles-sidebar.js +++ b/packages/edit-site/src/components/sidebar-edit-mode/global-styles-sidebar.js @@ -132,13 +132,11 @@ export default function GlobalStylesSidebar() { closeLabel={ __( 'Close Styles' ) } panelClassName="edit-site-global-styles-sidebar__panel" header={ - + - { __( 'Styles' ) } +

    + { __( 'Styles' ) } +