From 236250e3fa04ad7337c305540f76ed619b0dde98 Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Thu, 8 Aug 2024 11:59:28 +0200 Subject: [PATCH 001/126] Components: add "Naming conventions" section (#63714) * Use update compound suggested notation * Remove reference to item group as it may not reflect anymore all recent best practices * Tweak part about experimental APIs * Fix spacing * Linter auto-formatting * Add Naming Conventions section * Update guidelines to use overloaded convention, remove references to monolithic components * Added more details to the JSX example * Grammar * Split unforwarded component from forwardRef call * Fix comment + auto-format * Add section on JSDocs + IntelliSense requirements + recommended best practices * Use named functions without attributing them to const --- Co-authored-by: ciampo Co-authored-by: tyxla Co-authored-by: mirka <0mirka00@git.wordpress.org> Co-authored-by: DaniGuardiola Co-authored-by: diegohaz Co-authored-by: youknowriad Co-authored-by: ntsekouras Co-authored-by: aaronrobertshaw Co-authored-by: noisysocks --- packages/components/CONTRIBUTING.md | 219 ++++++++++++++++++++-------- 1 file changed, 155 insertions(+), 64 deletions(-) diff --git a/packages/components/CONTRIBUTING.md b/packages/components/CONTRIBUTING.md index f1245da7a409c6..57a9f2fb049a24 100644 --- a/packages/components/CONTRIBUTING.md +++ b/packages/components/CONTRIBUTING.md @@ -6,20 +6,20 @@ The following is a set of guidelines for contributing to the `@wordpress/compone This set of guidelines should apply especially to newly introduced components. In fact, while these guidelines should also be retroactively applied to existing components, it is sometimes impossible to do so for legacy/compatibility reasons. -For an example of a component that follows these requirements, take a look at [`ItemGroup`](/packages/components/src/item-group). -- [Introducing new components](#introducing-new-components) -- [Compatibility](#compatibility) -- [Compound components](#compound-components) -- [Components & Hooks](#components--hooks) -- [TypeScript](#typescript) -- [Styling](#styling) -- [Context system](#context-system) -- [Unit tests](#unit-tests) -- [Storybook](#storybook) -- [Documentation](#documentation) -- [README example](#README-example) -- [Folder structure](#folder-structure) -- [Component versioning](#component-versioning) +- [Introducing new components](#introducing-new-components) +- [Compatibility](#compatibility) +- [Compound components](#compound-components) +- [Components & Hooks](#components--hooks) +- [Naming Conventions](#naming-conventions) +- [TypeScript](#typescript) +- [Styling](#styling) +- [Context system](#context-system) +- [Unit tests](#unit-tests) +- [Storybook](#storybook) +- [Documentation](#documentation) +- [README example](#README-example) +- [Folder structure](#folder-structure) +- [Component versioning](#component-versioning) ## Introducing new components @@ -95,13 +95,13 @@ In these situations, one possible approach is to "soft-deprecate" a given legacy 2. Updating all places in Gutenberg that use that API. 3. Adding deprecation warnings (only after the previous point is completed, otherwise the Browser Console will be polluted by all those warnings and some e2e tests may fail). -When adding new components or new props to existing components, it's recommended to prefix them with `__unstable` or `__experimental` until they're stable enough to be exposed as part of the public API. +When adding new components or new props to existing components, it's recommended to create a [private version](/packages/private-apis/README.md)) of the component until the changes are stable enough to be exposed as part of the public API. ### Learn more -- [How to preserve backward compatibility for a React Component](/docs/contributors/code/backward-compatibility.md#how-to-preserve-backward-compatibility-for-a-react-component) -- [Experimental and Unstable APIs](/docs/contributors/code/coding-guidelines.md#experimental-and-unstable-apis) -- [Deprecating styles](#deprecating-styles) +- [How to preserve backward compatibility for a React Component](/docs/contributors/code/backward-compatibility.md#how-to-preserve-backward-compatibility-for-a-react-component) +- [Experimental and Unstable APIs](/docs/contributors/code/coding-guidelines.md#legacy-experimental-apis-plugin-only-apis-and-private-apis) +- [Deprecating styles](#deprecating-styles) +## Naming Conventions + +It is recommended that compound components use dot notation to separate the namespace from the individual component names. The top-level compound component should be called the namespace (no dot notation). + +Dedicated React context should also use dot notation, while hooks should not. + +When exporting compound components and preparing them to be consumed, it is important that: + +- the JSDocs appear correctly in IntelliSense; +- the top-level component's JSDoc appears in the Storybook docs page; +- the top-level and subcomponent's prop types appear correctly in the Storybook props table. + +To meet the above requirements, we recommend: + +- using `Object.assign()` to add subcomponents as properties of the top-level component; +- using named functions for all components; +- setting explicitly the `displayName` on all subcomponents; +- adding the top-level JSDoc to the result of the `Object.assign` call; +- adding inline subcomponent JSDocs inside the `Object.assign` call. + +The following example implements all of the above recommendations. + +```tsx +//======================= +// Component.tsx +//======================= +import { forwardRef, createContext } from '@wordpress/element'; + +function UnforwardedTopLevelComponent( props, ref ) { + /* ... */ +} +const TopLevelComponent = forwardRef( UnforwardedTopLevelComponent ); + +function UnforwardedSubComponent( props, ref ) { + /* ... */ +} +const SubComponent = forwardRef( UnforwardedSubComponent ); +SubComponent.displayName = 'Component.SubComponent'; + +const Context = createContext(); + +/** The top-level component's JSDoc. */ +export const Component = Object.assign( TopLevelComponent, { + /** The subcomponent's JSDoc. */ + SubComponent, + /** The context's JSDoc. */ + Context, +} ); + +/** The hook's JSDoc. */ +export function useComponent() { + /* ... */ +} + +//======================= +// App.tsx +//======================= +import { Component, useComponent } from '@wordpress/components'; +import { useContext } from '@wordpress/element'; + +function CompoundComponentExample() { + return ( + + + + ); +} + +function ContextProviderExample() { + return ( + + { /* React tree */ } + + ); +} + +function ContextConsumerExample() { + const componentContext = useContext( Component.Context ); + + // etc +} + +function HookExample() { + const hookReturnValue = useComponent(); + + // etc. +} +``` + ## TypeScript We strongly encourage using TypeScript for all new components. @@ -278,8 +363,10 @@ function UnconnectedMyComponent( // parameter (`div` in this example) // - the special `as` prop (which marks the component as polymorphic), // unless the third parameter is `false` - props: WordPressComponentProps< ComponentOwnProps, 'div', true > -) { /* ... */ } + props: WordPressComponentProps< ComponentOwnProps, 'div', true > +) { + /* ... */ +} ``` ### Considerations for the docgen @@ -287,10 +374,15 @@ function UnconnectedMyComponent( Make sure you have a **named** export for the component, not just the default export ([example](https://github.com/WordPress/gutenberg/blob/trunk/packages/components/src/divider/component.tsx)). This ensures that the docgen can properly extract the types data. The naming should be so that the connected/forwarded component has the plain component name (`MyComponent`), and the raw component is prefixed (`UnconnectedMyComponent` or `UnforwardedMyComponent`). This makes the component's `displayName` look nicer in React devtools and in the autogenerated Storybook code snippets. ```js -function UnconnectedMyComponent() { /* ... */ } +function UnconnectedMyComponent() { + /* ... */ +} // 👇 Without this named export, the docgen will not work! -export const MyComponent = contextConnect( UnconnectedMyComponent, 'MyComponent' ); +export const MyComponent = contextConnect( + UnconnectedMyComponent, + 'MyComponent' +); export default MyComponent; ``` @@ -314,16 +406,15 @@ Changing the styles of a non-experimental component must be done with care. To p import deprecated from '@wordpress/deprecated'; import { Wrapper } from './styles.ts'; -function MyComponent({ __nextHasNoOuterMargins = false }) { +function MyComponent( { __nextHasNoOuterMargins = false } ) { if ( ! __nextHasNoOuterMargins ) { deprecated( 'Outer margin styles for wp.components.MyComponent', { since: '6.0', version: '6.2', // Set a reasonable grace period depending on impact - hint: - 'Set the `__nextHasNoOuterMargins` prop to true to start opting into the new styles, which will become the default in a future version.', + hint: 'Set the `__nextHasNoOuterMargins` prop to true to start opting into the new styles, which will become the default in a future version.', } ); } - return + return ; } ``` @@ -331,7 +422,7 @@ Styles should be structured so the deprecated styles are cleanly encapsulated, a ```js // styles.ts -const deprecatedMargins = ({ __nextHasNoOuterMargins }) => { +const deprecatedMargins = ( { __nextHasNoOuterMargins } ) => { if ( ! __nextHasNoOuterMargins ) { return css` margin: 8px; @@ -342,7 +433,7 @@ const deprecatedMargins = ({ __nextHasNoOuterMargins }) => { export const Wrapper = styled.div` margin: 0; - ${deprecatedMargins} + ${ deprecatedMargins } `; ``` @@ -358,14 +449,14 @@ Not all style changes justify a formal deprecation process. The main thing to lo ##### DOES need formal deprecation -- Removing an outer margin. -- Substantial changes to width/height, such as adding or removing a size restriction. +- Removing an outer margin. +- Substantial changes to width/height, such as adding or removing a size restriction. ##### DOES NOT need formal deprecation -- Breakage only occurs in non-standard usage, such as when the consumer is overriding component internals. -- Minor layout shifts of a few pixels. -- Internal layout changes of a higher-level component. +- Breakage only occurs in non-standard usage, such as when the consumer is overriding component internals. +- Minor layout shifts of a few pixels. +- Internal layout changes of a higher-level component. ## Context system @@ -373,9 +464,9 @@ The `@wordpress/components` context system is based on [React's `Context` API](h Components can use this system via a couple of functions: -- they can provide values using a shared `ContextSystemProvider` component -- they can connect to the Context via `contextConnect` -- they can read the "computed" values from the context via `useContextSystem` +- they can provide values using a shared `ContextSystemProvider` component +- they can connect to the Context via `contextConnect` +- they can read the "computed" values from the context via `useContextSystem` An example of how this is used can be found in the [`Card` component family](/packages/components/src/card). For example, this is how the `Card` component injects the `size` and `isBorderless` props down to its `CardBody` subcomponent — which makes it use the correct spacing and border settings "auto-magically". @@ -400,11 +491,7 @@ export function useCard( props ) { import { contextConnect, ContextSystemProvider } from '../../context'; function Card( props, forwardedRef ) { - const { - size, - isBorderless, - ...otherComputedHookProps - } = useCard( props ); + const { size, isBorderless, ...otherComputedHookProps } = useCard( props ); // [...] @@ -441,7 +528,10 @@ export function useCardBody( props ) { // If a `CardBody` component is rendered as a child of a `Card` component, the value of // the `size` prop will be the one set by the parent `Card` component via the Context // System (unless the prop gets explicitely set on the `CardBody` component). - const { size = 'medium', ...otherDerivedProps } = useContextSystem( props, 'CardBody' ); + const { size = 'medium', ...otherDerivedProps } = useContextSystem( + props, + 'CardBody' + ); // [...] @@ -457,7 +547,7 @@ Please refer to the [JavaScript Testing Overview docs](/docs/contributors/code/t All new components should add stories to the project's [Storybook](https://storybook.js.org/). Each [story](https://storybook.js.org/docs/react/get-started/whats-a-story) captures the rendered state of a UI component in isolation. This greatly simplifies working on a given component, while also serving as an interactive form of documentation. -A component's story should be showcasing its different states — for example, the different variants of a `Button`: +A component's story should be showcasing its different states — for example, the different variants of a `Button`: ```jsx import Button from '../'; @@ -543,6 +633,7 @@ Prop description. With a new line before and after the description and before an Add this section when there are props that are drilled down into an internal component. See [ClipboardButton](/packages/components/src/clipboard-button/README.md) for an example. + ## Context See examples for this section for the [ItemGroup](/packages/components/src/item-group/item-group/README.md#context) and [`Card`](/packages/components/src/card/card/README.md#context) components. @@ -601,8 +692,8 @@ As the needs of the package evolve with time, sometimes we may opt to fully rewr 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. +- "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 subcomponents) exported from the package, their props, any associated React context. It does not include internal classnames and internal DOM structure of the components. ### Approaches From 7661f457c867906d57ea19f05becef217a1ad31c Mon Sep 17 00:00:00 2001 From: Dave Smith Date: Thu, 8 Aug 2024 11:01:34 +0100 Subject: [PATCH 002/126] Stop unwanted drag and drop operations within section Patterns in Zoom Out mode (#64331) * Fix conditionals * Disable horizontal insertion points to prioritise dropping between sections --- .../components/block-tools/insertion-point.js | 11 +++++++++++ .../src/components/inner-blocks/index.js | 19 ++++++++++--------- 2 files changed, 21 insertions(+), 9 deletions(-) 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 9dac99e5e93124..469f7e53908cb4 100644 --- a/packages/block-editor/src/components/block-tools/insertion-point.js +++ b/packages/block-editor/src/components/block-tools/insertion-point.js @@ -38,6 +38,7 @@ function InbetweenInsertionPointPopover( { isInserterShown, isDistractionFree, isNavigationMode, + isZoomOutMode, } = useSelect( ( select ) => { const { getBlockOrder, @@ -48,6 +49,7 @@ function InbetweenInsertionPointPopover( { getNextBlockClientId, getSettings, isNavigationMode: _isNavigationMode, + __unstableGetEditorMode, } = select( blockEditorStore ); const insertionPoint = getBlockInsertionPoint(); const order = getBlockOrder( insertionPoint.rootClientId ); @@ -79,6 +81,7 @@ function InbetweenInsertionPointPopover( { isNavigationMode: _isNavigationMode(), isDistractionFree: settings.isDistractionFree, isInserterShown: insertionPoint?.__unstableWithInserter, + isZoomOutMode: __unstableGetEditorMode() === 'zoom-out', }; }, [] ); const { getBlockEditingMode } = useSelect( blockEditorStore ); @@ -145,6 +148,14 @@ function InbetweenInsertionPointPopover( { return null; } + // Zoom out mode should only show the insertion point for the insert operation. + // Other operations such as "group" are when the editor tries to create a row + // block by grouping the block being dragged with the block it's being dropped + // onto. + if ( isZoomOutMode && operation !== 'insert' ) { + return null; + } + const orientationClassname = orientation === 'horizontal' || operation === 'group' ? 'is-horizontal' diff --git a/packages/block-editor/src/components/inner-blocks/index.js b/packages/block-editor/src/components/inner-blocks/index.js index 27e5064eeb6328..c8db9f8cebf905 100644 --- a/packages/block-editor/src/components/inner-blocks/index.js +++ b/packages/block-editor/src/components/inner-blocks/index.js @@ -206,13 +206,7 @@ export function useInnerBlocksProps( props = {}, options = {} ) { getSettings, } = unlock( select( blockEditorStore ) ); let _isDropZoneDisabled; - // In zoom out mode, we want to disable the drop zone for the sections. - // The inner blocks belonging to the section drop zone is - // already disabled by the blocks themselves being disabled. - if ( __unstableGetEditorMode() === 'zoom-out' ) { - const { sectionRootClientId } = unlock( getSettings() ); - _isDropZoneDisabled = clientId !== sectionRootClientId; - } + if ( ! clientId ) { return { isDropZoneDisabled: _isDropZoneDisabled }; } @@ -225,8 +219,15 @@ export function useInnerBlocksProps( props = {}, options = {} ) { const parentClientId = getBlockRootClientId( clientId ); const [ defaultLayout ] = getBlockSettings( clientId, 'layout' ); - if ( _isDropZoneDisabled !== undefined ) { - _isDropZoneDisabled = blockEditingMode === 'disabled'; + _isDropZoneDisabled = blockEditingMode === 'disabled'; + + if ( __unstableGetEditorMode() === 'zoom-out' ) { + // In zoom out mode, we want to disable the drop zone for the sections. + // The inner blocks belonging to the section drop zone is + // already disabled by the blocks themselves being disabled. + const { sectionRootClientId } = unlock( getSettings() ); + + _isDropZoneDisabled = clientId !== sectionRootClientId; } return { From 098c1a2e45d3daa1353af6e26e9f518a06392865 Mon Sep 17 00:00:00 2001 From: Jarda Snajdr Date: Thu, 8 Aug 2024 13:14:59 +0200 Subject: [PATCH 003/126] DataViews: abandon the ItemRecord type (#64367) Co-authored-by: jsnajdr Co-authored-by: youknowriad --- packages/dataviews/src/normalize-fields.ts | 6 ++---- packages/dataviews/src/types.ts | 24 +++++++--------------- 2 files changed, 9 insertions(+), 21 deletions(-) diff --git a/packages/dataviews/src/normalize-fields.ts b/packages/dataviews/src/normalize-fields.ts index 8cd9051cbb1cc3..680749df5344a6 100644 --- a/packages/dataviews/src/normalize-fields.ts +++ b/packages/dataviews/src/normalize-fields.ts @@ -2,7 +2,7 @@ * Internal dependencies */ import getFieldTypeDefinition from './field-types'; -import type { Field, NormalizedField, ItemRecord } from './types'; +import type { Field, NormalizedField } from './types'; /** * Apply default values and normalize the fields config. @@ -17,9 +17,7 @@ export function normalizeFields< Item >( const fieldTypeDefinition = getFieldTypeDefinition( field.type ); const getValue = - field.getValue || - // @ts-ignore - ( ( { item }: { item: ItemRecord } ) => item[ field.id ] ); + field.getValue || ( ( { item } ) => ( item as any )[ field.id ] ); const sort = field.sort ?? diff --git a/packages/dataviews/src/types.ts b/packages/dataviews/src/types.ts index 64580a089997d1..b0873e9c677f53 100644 --- a/packages/dataviews/src/types.ts +++ b/packages/dataviews/src/types.ts @@ -47,8 +47,6 @@ export type Operator = | 'isAll' | 'isNotAll'; -export type ItemRecord = Object; - export type FieldType = 'text' | 'integer'; export type ValidationContext = { @@ -128,21 +126,13 @@ export type Field< Item > = { * Filter config for the field. */ filterBy?: FilterByConfig | undefined; -} & ( Item extends ItemRecord - ? { - /** - * Callback used to retrieve the value of the field from the item. - * Defaults to `item[ field.id ]`. - */ - getValue?: ( args: { item: Item } ) => any; - } - : { - /** - * Callback used to retrieve the value of the field from the item. - * Defaults to `item[ field.id ]`. - */ - getValue: ( args: { item: Item } ) => any; - } ); + + /** + * Callback used to retrieve the value of the field from the item. + * Defaults to `item[ field.id ]`. + */ + getValue?: ( args: { item: Item } ) => any; +}; export type NormalizedField< Item > = Field< Item > & { label: string; From 3a21676ea456e3484949475c9b654de3e252ac45 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Thu, 8 Aug 2024 13:16:01 +0200 Subject: [PATCH 004/126] DataViews Extensibility: Allow unregistering the rename post action (#64366) Co-authored-by: youknowriad Co-authored-by: ntsekouras --- .../src/components/post-actions/actions.js | 132 +--------------- .../src/dataviews/actions/rename-post.tsx | 146 ++++++++++++++++++ .../editor/src/dataviews/actions/utils.ts | 14 +- .../src/dataviews/store/private-actions.ts | 4 +- packages/editor/src/dataviews/types.ts | 16 +- 5 files changed, 174 insertions(+), 138 deletions(-) create mode 100644 packages/editor/src/dataviews/actions/rename-post.tsx diff --git a/packages/editor/src/components/post-actions/actions.js b/packages/editor/src/components/post-actions/actions.js index 64afec2417cd05..490bab2cc2fa4b 100644 --- a/packages/editor/src/components/post-actions/actions.js +++ b/packages/editor/src/components/post-actions/actions.js @@ -14,7 +14,6 @@ import { parse } from '@wordpress/blocks'; import { DataForm } from '@wordpress/dataviews'; import { Button, - TextControl, __experimentalHStack as HStack, __experimentalVStack as VStack, } from '@wordpress/components'; @@ -23,7 +22,6 @@ import { * Internal dependencies */ import { - TEMPLATE_ORIGINS, TEMPLATE_PART_POST_TYPE, TEMPLATE_POST_TYPE, PATTERN_POST_TYPE, @@ -34,7 +32,7 @@ import { CreateTemplatePartModalContents } from '../create-template-part-modal'; import { getItemTitle } from '../../dataviews/actions/utils'; // Patterns. -const { PATTERN_TYPES, CreatePatternModalContents, useDuplicatePatternProps } = +const { CreatePatternModalContents, useDuplicatePatternProps } = unlock( patternsPrivateApis ); // TODO: this should be shared with other components (see post-fields in edit-site). @@ -58,25 +56,6 @@ const formDuplicateAction = { fields: [ 'title' ], }; -/** - * Check if a template is removable. - * - * @param {Object} template The template entity to check. - * @return {boolean} Whether the template is removable. - */ -function isTemplateRemovable( template ) { - if ( ! template ) { - return false; - } - // In patterns list page we map the templates parts to a different object - // than the one returned from the endpoint. This is why we need to check for - // two props whether is custom or has a theme file. - return ( - template?.source === TEMPLATE_ORIGINS.custom && - ! template?.has_theme_file - ); -} - const viewPostAction = { id: 'view-post', label: __( 'View' ), @@ -128,112 +107,6 @@ const postRevisionsAction = { }, }; -const renamePostAction = { - id: 'rename-post', - label: __( 'Rename' ), - isEligible( post ) { - if ( post.status === 'trash' ) { - return false; - } - // Templates, template parts and patterns have special checks for renaming. - if ( - ! [ - TEMPLATE_POST_TYPE, - TEMPLATE_PART_POST_TYPE, - ...Object.values( PATTERN_TYPES ), - ].includes( post.type ) - ) { - return post.permissions?.update; - } - // In the case of templates, we can only rename custom templates. - if ( post.type === TEMPLATE_POST_TYPE ) { - return ( - isTemplateRemovable( post ) && - post.is_custom && - post.permissions?.update - ); - } - // Make necessary checks for template parts and patterns. - const isTemplatePart = post.type === TEMPLATE_PART_POST_TYPE; - const isUserPattern = post.type === PATTERN_TYPES.user; - // In patterns list page we map the templates parts to a different object - // than the one returned from the endpoint. This is why we need to check for - // two props whether is custom or has a theme file. - const isCustomPattern = - isUserPattern || - ( isTemplatePart && post.source === TEMPLATE_ORIGINS.custom ); - const hasThemeFile = post?.has_theme_file; - return isCustomPattern && ! hasThemeFile && post.permissions?.update; - }, - RenderModal: ( { items, closeModal, onActionPerformed } ) => { - const [ item ] = items; - const [ title, setTitle ] = useState( () => getItemTitle( item ) ); - const { editEntityRecord, saveEditedEntityRecord } = - useDispatch( coreStore ); - const { createSuccessNotice, createErrorNotice } = - useDispatch( noticesStore ); - - async function onRename( event ) { - event.preventDefault(); - try { - await editEntityRecord( 'postType', item.type, item.id, { - title, - } ); - // Update state before saving rerenders the list. - setTitle( '' ); - closeModal(); - // Persist edited entity. - await saveEditedEntityRecord( 'postType', item.type, item.id, { - throwOnError: true, - } ); - createSuccessNotice( __( 'Name updated' ), { - type: 'snackbar', - } ); - onActionPerformed?.( items ); - } catch ( error ) { - const errorMessage = - error.message && error.code !== 'unknown_error' - ? error.message - : __( 'An error occurred while updating the name' ); - createErrorNotice( errorMessage, { type: 'snackbar' } ); - } - } - - return ( -
- - - - - - - -
- ); - }, -}; - const useDuplicatePostAction = ( postType ) => { const userCanCreatePost = useSelect( ( select ) => { @@ -494,7 +367,6 @@ export function usePostActions( { postType, onActionPerformed, context } ) { const isPattern = postType === PATTERN_POST_TYPE; const isLoaded = !! postTypeObject; const supportsRevisions = !! postTypeObject?.supports?.revisions; - const supportsTitle = !! postTypeObject?.supports?.title; return useMemo( () => { if ( ! isLoaded ) { return []; @@ -512,7 +384,6 @@ export function usePostActions( { postType, onActionPerformed, context } ) { userCanCreatePostType && duplicateTemplatePartAction, isPattern && userCanCreatePostType && duplicatePatternAction, - supportsTitle && renamePostAction, ...defaultActions, ].filter( Boolean ); // Filter actions based on provided context. If not provided @@ -586,7 +457,6 @@ export function usePostActions( { postType, onActionPerformed, context } ) { onActionPerformed, isLoaded, supportsRevisions, - supportsTitle, context, ] ); } diff --git a/packages/editor/src/dataviews/actions/rename-post.tsx b/packages/editor/src/dataviews/actions/rename-post.tsx new file mode 100644 index 00000000000000..ef9da271111ea2 --- /dev/null +++ b/packages/editor/src/dataviews/actions/rename-post.tsx @@ -0,0 +1,146 @@ +/** + * WordPress dependencies + */ +import { useDispatch } from '@wordpress/data'; +import { store as coreStore } from '@wordpress/core-data'; +import { __ } from '@wordpress/i18n'; +import { useState } from '@wordpress/element'; +// @ts-ignore +import { privateApis as patternsPrivateApis } from '@wordpress/patterns'; +import { + Button, + TextControl, + __experimentalHStack as HStack, + __experimentalVStack as VStack, +} from '@wordpress/components'; +import type { Action } from '@wordpress/dataviews'; +import { store as noticesStore } from '@wordpress/notices'; + +/** + * Internal dependencies + */ +import { + TEMPLATE_ORIGINS, + TEMPLATE_PART_POST_TYPE, + TEMPLATE_POST_TYPE, +} from '../../store/constants'; +import { unlock } from '../../lock-unlock'; +import { + getItemTitle, + isTemplateRemovable, + isTemplate, + isTemplatePart, +} from './utils'; +import type { CoreDataError, PostWithPermissions } from '../types'; + +// Patterns. +const { PATTERN_TYPES } = unlock( patternsPrivateApis ); + +const renamePost: Action< PostWithPermissions > = { + id: 'rename-post', + label: __( 'Rename' ), + isEligible( post ) { + if ( post.status === 'trash' ) { + return false; + } + // Templates, template parts and patterns have special checks for renaming. + if ( + ! [ + TEMPLATE_POST_TYPE, + TEMPLATE_PART_POST_TYPE, + ...Object.values( PATTERN_TYPES ), + ].includes( post.type ) + ) { + return post.permissions?.update; + } + + // In the case of templates, we can only rename custom templates. + if ( isTemplate( post ) ) { + return ( + isTemplateRemovable( post ) && + post.is_custom && + post.permissions?.update + ); + } + + if ( isTemplatePart( post ) ) { + return ( + post.source === TEMPLATE_ORIGINS.custom && + ! post?.has_theme_file && + post.permissions?.update + ); + } + + return post.type === PATTERN_TYPES.user && post.permissions?.update; + }, + RenderModal: ( { items, closeModal, onActionPerformed } ) => { + const [ item ] = items; + const [ title, setTitle ] = useState( () => getItemTitle( item ) ); + const { editEntityRecord, saveEditedEntityRecord } = + useDispatch( coreStore ); + const { createSuccessNotice, createErrorNotice } = + useDispatch( noticesStore ); + + async function onRename( event: React.FormEvent ) { + event.preventDefault(); + try { + await editEntityRecord( 'postType', item.type, item.id, { + title, + } ); + // Update state before saving rerenders the list. + setTitle( '' ); + closeModal?.(); + // Persist edited entity. + await saveEditedEntityRecord( 'postType', item.type, item.id, { + throwOnError: true, + } ); + createSuccessNotice( __( 'Name updated' ), { + type: 'snackbar', + } ); + onActionPerformed?.( items ); + } catch ( error ) { + const typedError = error as CoreDataError; + const errorMessage = + typedError.message && typedError.code !== 'unknown_error' + ? typedError.message + : __( 'An error occurred while updating the name' ); + createErrorNotice( errorMessage, { type: 'snackbar' } ); + } + } + + return ( +
+ + + + + + + +
+ ); + }, +}; + +export default renamePost; diff --git a/packages/editor/src/dataviews/actions/utils.ts b/packages/editor/src/dataviews/actions/utils.ts index 56c8c9f54c8507..7da1f71728365b 100644 --- a/packages/editor/src/dataviews/actions/utils.ts +++ b/packages/editor/src/dataviews/actions/utils.ts @@ -12,11 +12,19 @@ import { TEMPLATE_POST_TYPE, } from '../../store/constants'; -import type { Post, TemplateOrTemplatePart } from '../types'; +import type { Post, TemplatePart, Template } from '../types'; + +export function isTemplate( post: Post ): post is Template { + return post.type === TEMPLATE_POST_TYPE; +} + +export function isTemplatePart( post: Post ): post is TemplatePart { + return post.type === TEMPLATE_PART_POST_TYPE; +} export function isTemplateOrTemplatePart( p: Post -): p is TemplateOrTemplatePart { +): p is Template | TemplatePart { return p.type === TEMPLATE_POST_TYPE || p.type === TEMPLATE_PART_POST_TYPE; } @@ -39,7 +47,7 @@ export function getItemTitle( item: Post ) { * @param template The template entity to check. * @return Whether the template is removable. */ -export function isTemplateRemovable( template: TemplateOrTemplatePart ) { +export function isTemplateRemovable( template: Template | TemplatePart ) { if ( ! template ) { return false; } diff --git a/packages/editor/src/dataviews/store/private-actions.ts b/packages/editor/src/dataviews/store/private-actions.ts index d5e12e298039a1..a8b1573a528b50 100644 --- a/packages/editor/src/dataviews/store/private-actions.ts +++ b/packages/editor/src/dataviews/store/private-actions.ts @@ -13,8 +13,9 @@ import exportPattern from '../actions/export-pattern'; import resetPost from '../actions/reset-post'; import trashPost from '../actions/trash-post'; import permanentlyDeletePost from '../actions/permanently-delete-post'; -import restorePost from '../actions/restore-post'; +import renamePost from '../actions/rename-post'; import reorderPage from '../actions/reorder-page'; +import restorePost from '../actions/restore-post'; import type { PostType } from '../types'; import { store as editorStore } from '../../store'; import { unlock } from '../../lock-unlock'; @@ -74,6 +75,7 @@ export const registerPostTypeActions = .getPostType( postType ) ) as PostType; const actions = [ + postTypeConfig.supports?.title ? renamePost : undefined, postTypeConfig?.supports?.[ 'page-attributes' ] ? reorderPage : undefined, diff --git a/packages/editor/src/dataviews/types.ts b/packages/editor/src/dataviews/types.ts index 0c31bef195eb9b..80b6f3c5ceb852 100644 --- a/packages/editor/src/dataviews/types.ts +++ b/packages/editor/src/dataviews/types.ts @@ -13,8 +13,17 @@ export interface BasePost { type: string; id: string | number; } -export interface TemplateOrTemplatePart extends BasePost { - type: 'wp_template' | 'wp_template_part'; + +export interface Template extends BasePost { + type: 'wp_template'; + is_custom: boolean; + source: string; + has_theme_file: boolean; + id: string; +} + +export interface TemplatePart extends BasePost { + type: 'wp_template_part'; source: string; has_theme_file: boolean; id: string; @@ -31,7 +40,7 @@ export interface PostWithPageAttributesSupport extends BasePost { menu_order: number; } -export type Post = TemplateOrTemplatePart | Pattern | BasePost; +export type Post = Template | TemplatePart | Pattern | BasePost; export type PostWithPermissions = Post & { permissions: { @@ -44,6 +53,7 @@ export interface PostType { slug: string; supports?: { 'page-attributes'?: boolean; + title?: boolean; }; } From 5d408ce8849e6183cb39f48b071400b04a1cde78 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Thu, 8 Aug 2024 16:19:58 +0200 Subject: [PATCH 005/126] DataViews Extensibility: Allow unregistering the duplicate pattern action (#64373) Co-authored-by: youknowriad Co-authored-by: ntsekouras --- .../src/components/post-actions/actions.js | 27 ------------- .../dataviews/actions/duplicate-pattern.tsx | 40 +++++++++++++++++++ .../src/dataviews/store/private-actions.ts | 11 +++++ 3 files changed, 51 insertions(+), 27 deletions(-) create mode 100644 packages/editor/src/dataviews/actions/duplicate-pattern.tsx diff --git a/packages/editor/src/components/post-actions/actions.js b/packages/editor/src/components/post-actions/actions.js index 490bab2cc2fa4b..fcad5b97a13002 100644 --- a/packages/editor/src/components/post-actions/actions.js +++ b/packages/editor/src/components/post-actions/actions.js @@ -9,7 +9,6 @@ import { store as coreStore } from '@wordpress/core-data'; import { __, sprintf, _x } from '@wordpress/i18n'; import { store as noticesStore } from '@wordpress/notices'; import { useMemo, useState, useEffect } from '@wordpress/element'; -import { privateApis as patternsPrivateApis } from '@wordpress/patterns'; import { parse } from '@wordpress/blocks'; import { DataForm } from '@wordpress/dataviews'; import { @@ -31,10 +30,6 @@ import { unlock } from '../../lock-unlock'; import { CreateTemplatePartModalContents } from '../create-template-part-modal'; import { getItemTitle } from '../../dataviews/actions/utils'; -// Patterns. -const { CreatePatternModalContents, useDuplicatePatternProps } = - unlock( patternsPrivateApis ); - // TODO: this should be shared with other components (see post-fields in edit-site). const fields = [ { @@ -268,27 +263,6 @@ const useDuplicatePostAction = ( postType ) => { ); }; -export const duplicatePatternAction = { - id: 'duplicate-pattern', - label: _x( 'Duplicate', 'action label' ), - isEligible: ( item ) => item.type !== TEMPLATE_PART_POST_TYPE, - modalHeader: _x( 'Duplicate pattern', 'action label' ), - RenderModal: ( { items, closeModal } ) => { - const [ item ] = items; - const duplicatedProps = useDuplicatePatternProps( { - pattern: item, - onSuccess: () => closeModal(), - } ); - return ( - - ); - }, -}; - export const duplicateTemplatePartAction = { id: 'duplicate-template-part', label: _x( 'Duplicate', 'action label' ), @@ -383,7 +357,6 @@ export function usePostActions( { postType, onActionPerformed, context } ) { isTemplateOrTemplatePart && userCanCreatePostType && duplicateTemplatePartAction, - isPattern && userCanCreatePostType && duplicatePatternAction, ...defaultActions, ].filter( Boolean ); // Filter actions based on provided context. If not provided diff --git a/packages/editor/src/dataviews/actions/duplicate-pattern.tsx b/packages/editor/src/dataviews/actions/duplicate-pattern.tsx new file mode 100644 index 00000000000000..98f43a27c3628c --- /dev/null +++ b/packages/editor/src/dataviews/actions/duplicate-pattern.tsx @@ -0,0 +1,40 @@ +/** + * WordPress dependencies + */ +import { _x } from '@wordpress/i18n'; +// @ts-ignore +import { privateApis as patternsPrivateApis } from '@wordpress/patterns'; +import type { Action } from '@wordpress/dataviews'; + +/** + * Internal dependencies + */ +import { unlock } from '../../lock-unlock'; +import type { Pattern } from '../types'; + +// Patterns. +const { CreatePatternModalContents, useDuplicatePatternProps } = + unlock( patternsPrivateApis ); + +const duplicatePattern: Action< Pattern > = { + id: 'duplicate-pattern', + label: _x( 'Duplicate', 'action label' ), + isEligible: ( item ) => item.type !== 'wp_template_part', + modalHeader: _x( 'Duplicate pattern', 'action label' ), + RenderModal: ( { items, closeModal } ) => { + const [ item ] = items; + const duplicatedProps = useDuplicatePatternProps( { + pattern: item, + onSuccess: () => closeModal?.(), + } ); + return ( + + ); + }, +}; + +export default duplicatePattern; diff --git a/packages/editor/src/dataviews/store/private-actions.ts b/packages/editor/src/dataviews/store/private-actions.ts index a8b1573a528b50..6854c29bb0c4e4 100644 --- a/packages/editor/src/dataviews/store/private-actions.ts +++ b/packages/editor/src/dataviews/store/private-actions.ts @@ -9,6 +9,7 @@ import { doAction } from '@wordpress/hooks'; * Internal dependencies */ import deletePost from '../actions/delete-post'; +import duplicatePattern from '../actions/duplicate-pattern'; import exportPattern from '../actions/export-pattern'; import resetPost from '../actions/reset-post'; import trashPost from '../actions/trash-post'; @@ -74,7 +75,17 @@ export const registerPostTypeActions = .resolveSelect( coreStore ) .getPostType( postType ) ) as PostType; + const canCreate = await registry + .resolveSelect( coreStore ) + .canUser( 'create', { + kind: 'postType', + name: postType, + } ); + const actions = [ + canCreate && postTypeConfig.slug === 'wp_block' + ? duplicatePattern + : undefined, postTypeConfig.supports?.title ? renamePost : undefined, postTypeConfig?.supports?.[ 'page-attributes' ] ? reorderPage From c48075b6665ec3910d00677088672c1ba9e24916 Mon Sep 17 00:00:00 2001 From: James Koster Date: Thu, 8 Aug 2024 16:21:26 +0100 Subject: [PATCH 006/126] Reduce gap between steps in SpacingSizesControl, add animation, remove first/last marks (#63803) Co-authored-by: jameskoster Co-authored-by: stokesman Co-authored-by: richtabor Co-authored-by: paaljoachim Co-authored-by: bgardner --- .../input-controls/spacing-input-control.js | 10 +++++---- .../spacing-sizes-control/style.scss | 22 ++++++++++++++----- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/packages/block-editor/src/components/spacing-sizes-control/input-controls/spacing-input-control.js b/packages/block-editor/src/components/spacing-sizes-control/input-controls/spacing-input-control.js index 4faf05ba254089..5cdfe47c452b23 100644 --- a/packages/block-editor/src/components/spacing-sizes-control/input-controls/spacing-input-control.js +++ b/packages/block-editor/src/components/spacing-sizes-control/input-controls/spacing-input-control.js @@ -188,10 +188,12 @@ export default function SpacingInputControl( { name: size.name, } ) ); - const marks = spacingSizes.map( ( _newValue, index ) => ( { - value: index, - label: undefined, - } ) ); + const marks = spacingSizes + .slice( 1, spacingSizes.length - 1 ) + .map( ( _newValue, index ) => ( { + value: index + 1, + label: undefined, + } ) ); const sideLabel = ALL_SIDES.includes( side ) && showSideInLabel ? LABELS[ side ] : ''; diff --git a/packages/block-editor/src/components/spacing-sizes-control/style.scss b/packages/block-editor/src/components/spacing-sizes-control/style.scss index 2fb33b5c565fba..69f30a8c7d1e90 100644 --- a/packages/block-editor/src/components/spacing-sizes-control/style.scss +++ b/packages/block-editor/src/components/spacing-sizes-control/style.scss @@ -4,6 +4,18 @@ margin-bottom: 0; } + .is-marked { + .components-range-control__track { + transition: width ease 0.1s; + @include reduce-motion("transition"); + } + + .components-range-control__thumb-wrapper { + transition: left ease 0.1s; + @include reduce-motion("transition"); + } + } + .spacing-sizes-control__range-control, .spacing-sizes-control__custom-value-range { height: 40px; @@ -20,18 +32,16 @@ } .components-range-control__mark { + transform: translateX(-50%); height: $grid-unit-05; - width: 3px; - background-color: #fff; + width: math.div($grid-unit-05, 2); + background-color: $white; z-index: 1; + top: -#{$grid-unit-05}; } .components-range-control__marks { margin-top: 17px; - - :first-child { - display: none; - } } .components-range-control__thumb-wrapper { From 88127e20ff7386ae75417ee4c6f5e19b11d542ca Mon Sep 17 00:00:00 2001 From: George Mamadashvili Date: Thu, 8 Aug 2024 20:30:54 +0400 Subject: [PATCH 007/126] Block Editor: Don't hide the toolbar for an empty default block in HTML mode (#64374) Unlinked contributors: designsimply. Co-authored-by: Mamaduka Co-authored-by: stokesman Co-authored-by: Thelmachido Co-authored-by: mboynes --- .../src/components/block-tools/use-show-block-tools.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/block-editor/src/components/block-tools/use-show-block-tools.js b/packages/block-editor/src/components/block-tools/use-show-block-tools.js index 33807445b8da74..07e0ebd16a64b0 100644 --- a/packages/block-editor/src/components/block-tools/use-show-block-tools.js +++ b/packages/block-editor/src/components/block-tools/use-show-block-tools.js @@ -20,6 +20,7 @@ export function useShowBlockTools() { getSelectedBlockClientId, getFirstMultiSelectedBlockClientId, getBlock, + getBlockMode, getSettings, hasMultiSelection, __unstableGetEditorMode, @@ -33,7 +34,9 @@ export function useShowBlockTools() { const editorMode = __unstableGetEditorMode(); const hasSelectedBlock = !! clientId && !! block; const isEmptyDefaultBlock = - hasSelectedBlock && isUnmodifiedDefaultBlock( block ); + hasSelectedBlock && + isUnmodifiedDefaultBlock( block ) && + getBlockMode( clientId ) !== 'html'; const _showEmptyBlockSideInserter = clientId && ! isTyping() && From c495c7d21e3d0c3a6d3981a332202a0dd5d11ba5 Mon Sep 17 00:00:00 2001 From: George Mamadashvili Date: Thu, 8 Aug 2024 20:34:09 +0400 Subject: [PATCH 008/126] Upgrade Playwright to v1.46 (#64372) Co-authored-by: Mamaduka Co-authored-by: swissspidy --- package-lock.json | 48 +++++++++++++++++------------------ package.json | 2 +- packages/scripts/package.json | 2 +- 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/package-lock.json b/package-lock.json index b0eccc961afb78..66c32076d3b364 100644 --- a/package-lock.json +++ b/package-lock.json @@ -99,7 +99,7 @@ "@octokit/rest": "16.26.0", "@octokit/types": "6.34.0", "@octokit/webhooks-types": "5.8.0", - "@playwright/test": "1.45.1", + "@playwright/test": "1.46.0", "@pmmmwh/react-refresh-webpack-plugin": "0.5.11", "@react-native/babel-preset": "0.73.10", "@react-native/metro-babel-transformer": "0.73.10", @@ -6950,12 +6950,12 @@ } }, "node_modules/@playwright/test": { - "version": "1.45.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.45.1.tgz", - "integrity": "sha512-Wo1bWTzQvGA7LyKGIZc8nFSTFf2TkthGIFBR+QVNilvwouGzFd4PYukZe3rvf5PSqjHi1+1NyKSDZKcQWETzaA==", + "version": "1.46.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.46.0.tgz", + "integrity": "sha512-/QYft5VArOrGRP5pgkrfKksqsKA6CEFyGQ/gjNe6q0y4tZ1aaPfq4gIjudr1s3D+pXyrPRdsy4opKDrjBabE5w==", "dev": true, "dependencies": { - "playwright": "1.45.1" + "playwright": "1.46.0" }, "bin": { "playwright": "cli.js" @@ -41083,12 +41083,12 @@ "dev": true }, "node_modules/playwright": { - "version": "1.45.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.45.1.tgz", - "integrity": "sha512-Hjrgae4kpSQBr98nhCj3IScxVeVUixqj+5oyif8TdIn2opTCPEzqAqNMeK42i3cWDCVu9MI+ZsGWw+gVR4ISBg==", + "version": "1.46.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.46.0.tgz", + "integrity": "sha512-XYJ5WvfefWONh1uPAUAi0H2xXV5S3vrtcnXe6uAOgdGi3aSpqOSXX08IAjXW34xitfuOJsvXU5anXZxPSEQiJw==", "dev": true, "dependencies": { - "playwright-core": "1.45.1" + "playwright-core": "1.46.0" }, "bin": { "playwright": "cli.js" @@ -41101,9 +41101,9 @@ } }, "node_modules/playwright-core": { - "version": "1.45.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.45.1.tgz", - "integrity": "sha512-LF4CUUtrUu2TCpDw4mcrAIuYrEjVDfT1cHbJMfwnE2+1b8PZcFzPNgvZCvq2JfQ4aTjRCCHw5EJ2tmr2NSzdPg==", + "version": "1.46.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.46.0.tgz", + "integrity": "sha512-9Y/d5UIwuJk8t3+lhmMSAJyNP1BUC/DqP3cQJDQQL/oWqAiuPTLgy7Q5dzglmTLwcBRdetzgNM/gni7ckfTr6A==", "dev": true, "bin": { "playwright-core": "cli.js" @@ -54585,7 +54585,7 @@ "npm": ">=8.19.2" }, "peerDependencies": { - "@playwright/test": "^1.45.1", + "@playwright/test": "^1.46.0", "react": "^18.0.0", "react-dom": "^18.0.0" } @@ -60001,12 +60001,12 @@ } }, "@playwright/test": { - "version": "1.45.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.45.1.tgz", - "integrity": "sha512-Wo1bWTzQvGA7LyKGIZc8nFSTFf2TkthGIFBR+QVNilvwouGzFd4PYukZe3rvf5PSqjHi1+1NyKSDZKcQWETzaA==", + "version": "1.46.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.46.0.tgz", + "integrity": "sha512-/QYft5VArOrGRP5pgkrfKksqsKA6CEFyGQ/gjNe6q0y4tZ1aaPfq4gIjudr1s3D+pXyrPRdsy4opKDrjBabE5w==", "dev": true, "requires": { - "playwright": "1.45.1" + "playwright": "1.46.0" } }, "@pmmmwh/react-refresh-webpack-plugin": { @@ -87337,19 +87337,19 @@ "dev": true }, "playwright": { - "version": "1.45.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.45.1.tgz", - "integrity": "sha512-Hjrgae4kpSQBr98nhCj3IScxVeVUixqj+5oyif8TdIn2opTCPEzqAqNMeK42i3cWDCVu9MI+ZsGWw+gVR4ISBg==", + "version": "1.46.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.46.0.tgz", + "integrity": "sha512-XYJ5WvfefWONh1uPAUAi0H2xXV5S3vrtcnXe6uAOgdGi3aSpqOSXX08IAjXW34xitfuOJsvXU5anXZxPSEQiJw==", "dev": true, "requires": { "fsevents": "2.3.2", - "playwright-core": "1.45.1" + "playwright-core": "1.46.0" } }, "playwright-core": { - "version": "1.45.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.45.1.tgz", - "integrity": "sha512-LF4CUUtrUu2TCpDw4mcrAIuYrEjVDfT1cHbJMfwnE2+1b8PZcFzPNgvZCvq2JfQ4aTjRCCHw5EJ2tmr2NSzdPg==", + "version": "1.46.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.46.0.tgz", + "integrity": "sha512-9Y/d5UIwuJk8t3+lhmMSAJyNP1BUC/DqP3cQJDQQL/oWqAiuPTLgy7Q5dzglmTLwcBRdetzgNM/gni7ckfTr6A==", "dev": true }, "please-upgrade-node": { diff --git a/package.json b/package.json index fac57093a852c9..e4466eb4f470e1 100644 --- a/package.json +++ b/package.json @@ -111,7 +111,7 @@ "@octokit/rest": "16.26.0", "@octokit/types": "6.34.0", "@octokit/webhooks-types": "5.8.0", - "@playwright/test": "1.45.1", + "@playwright/test": "1.46.0", "@pmmmwh/react-refresh-webpack-plugin": "0.5.11", "@react-native/babel-preset": "0.73.10", "@react-native/metro-babel-transformer": "0.73.10", diff --git a/packages/scripts/package.json b/packages/scripts/package.json index 675d6aa9feb6d5..ff8e26b0e00c12 100644 --- a/packages/scripts/package.json +++ b/packages/scripts/package.json @@ -93,7 +93,7 @@ "webpack-dev-server": "^4.15.1" }, "peerDependencies": { - "@playwright/test": "^1.45.1", + "@playwright/test": "^1.46.0", "react": "^18.0.0", "react-dom": "^18.0.0" }, From 025125bc3734a46b2a6423f7e4287355032c2e2b Mon Sep 17 00:00:00 2001 From: Ramon Date: Fri, 9 Aug 2024 10:48:56 +1000 Subject: [PATCH 009/126] Background image: ensure consistency with defaults and fix reset/remove functionality (#64328) * Fixes miscellaneous bugs in the background image default values and controls. - checks global styles for uploaded images and applies defaults - changes the default position from "center" to "50% 50%" so it displays in the controls - ensures that reset/remove functionality closes the panel and removes image - do not save already-inherited styles to the block, and don't apply defaults where an inherited style already exists. * Reduce vertical height a smidge * Ensure defaults are accurately displayed in the background control panel. Add defaults for position '50% 50%' for blocks so it can be displayed in the background panel controls Pass inherited values to setBackgroundStyleDefaults so as not apply/overwrite inherited styles. Tighten up logic in setBackgroundStyleDefaults so that background position isn't set when an inherited value is contain and the block style is something else. * Simplify default logic for blocks with uploaded images * Update tests. * Adds tests for setBackgroundStyleDefaults * Simplify the inheritence checks. For uploaded images in global styles and block supports, always apply the defaults. * Simplify the inheritence checks. For uploaded images, always apply the defaults. * Changelog * Revert background.php change and comment in background-panel.js Co-authored-by: ramonjd Co-authored-by: andrewserong --- backport-changelog/6.7/7137.md | 1 + lib/block-supports/background.php | 2 +- lib/class-wp-theme-json-gutenberg.php | 2 +- .../global-styles/background-panel.js | 74 +++++++++++++------ .../test/use-global-styles-output.js | 2 +- packages/block-editor/src/hooks/background.js | 39 +++++----- .../block-editor/src/hooks/test/background.js | 60 +++++++++++++++ .../components/global-styles/screen-block.js | 3 +- phpunit/block-supports/background-test.php | 24 +++--- phpunit/class-wp-theme-json-test.php | 2 +- 10 files changed, 151 insertions(+), 58 deletions(-) create mode 100644 packages/block-editor/src/hooks/test/background.js diff --git a/backport-changelog/6.7/7137.md b/backport-changelog/6.7/7137.md index 834cb29a21e6d9..1eba52ebaf5087 100644 --- a/backport-changelog/6.7/7137.md +++ b/backport-changelog/6.7/7137.md @@ -1,3 +1,4 @@ https://github.com/WordPress/wordpress-develop/pull/7137 * https://github.com/WordPress/gutenberg/pull/64192 +* https://github.com/WordPress/gutenberg/pull/64328 diff --git a/lib/block-supports/background.php b/lib/block-supports/background.php index 811608127f47ed..a1d99133c1fc09 100644 --- a/lib/block-supports/background.php +++ b/lib/block-supports/background.php @@ -62,7 +62,7 @@ function gutenberg_render_background_support( $block_content, $block ) { $background_styles['backgroundSize'] = $background_styles['backgroundSize'] ?? 'cover'; // If the background size is set to `contain` and no position is set, set the position to `center`. if ( 'contain' === $background_styles['backgroundSize'] && ! $background_styles['backgroundPosition'] ) { - $background_styles['backgroundPosition'] = 'center'; + $background_styles['backgroundPosition'] = '50% 50%'; } } diff --git a/lib/class-wp-theme-json-gutenberg.php b/lib/class-wp-theme-json-gutenberg.php index ad8722091c2d48..7558438dcbe745 100644 --- a/lib/class-wp-theme-json-gutenberg.php +++ b/lib/class-wp-theme-json-gutenberg.php @@ -2391,7 +2391,7 @@ protected static function compute_style_properties( $styles, $settings = array() $styles['background']['backgroundSize'] = $styles['background']['backgroundSize'] ?? 'cover'; // If the background size is set to `contain` and no position is set, set the position to `center`. if ( 'contain' === $styles['background']['backgroundSize'] && empty( $styles['background']['backgroundPosition'] ) ) { - $styles['background']['backgroundPosition'] = 'center'; + $styles['background']['backgroundPosition'] = '50% 50%'; } } $background_styles = gutenberg_style_engine_get_styles( array( 'background' => $styles['background'] ) ); diff --git a/packages/block-editor/src/components/global-styles/background-panel.js b/packages/block-editor/src/components/global-styles/background-panel.js index 61f572f970b762..906202206d1b76 100644 --- a/packages/block-editor/src/components/global-styles/background-panel.js +++ b/packages/block-editor/src/components/global-styles/background-panel.js @@ -268,8 +268,10 @@ function BackgroundImageControls( { style, inheritedValue, onRemoveImage = noop, + onResetImage = noop, displayInPanel, themeFileURIs, + defaultValues, } ) { const mediaUpload = useSelect( ( select ) => select( blockEditorStore ).getSettings().mediaUpload, @@ -319,12 +321,8 @@ function BackgroundImageControls( { } const sizeValue = - style?.background?.backgroundSize || - inheritedValue?.background?.backgroundSize; - const positionValue = - style?.background?.backgroundPosition || - inheritedValue?.background?.backgroundPosition; - + style?.background?.backgroundSize || defaultValues?.backgroundSize; + const positionValue = style?.background?.backgroundPosition; onChange( setImmutably( style, [ 'background' ], { ...style?.background, @@ -335,6 +333,12 @@ function BackgroundImageControls( { title: media.title || undefined, }, backgroundPosition: + /* + * A background image uploaded and set in the editor receives a default background position of '50% 0', + * when the background image size is the equivalent of "Tile". + * This is to increase the chance that the image's focus point is visible. + * This is in-editor only to assist with the user experience. + */ ! positionValue && ( 'auto' === sizeValue || ! sizeValue ) ? '50% 0' : positionValue, @@ -372,7 +376,9 @@ function BackgroundImageControls( { const onRemove = () => onChange( - setImmutably( style, [ 'background', 'backgroundImage' ], 'none' ) + setImmutably( style, [ 'background' ], { + backgroundImage: 'none', + } ) ); const canRemove = ! hasValue && hasBackgroundImageValue( inheritedValue ); const imgLabel = @@ -413,6 +419,7 @@ function BackgroundImageControls( { onClick={ () => { closeAndFocus(); onRemove(); + onRemoveImage(); } } > { __( 'Remove' ) } @@ -422,7 +429,7 @@ function BackgroundImageControls( { { closeAndFocus(); - onRemoveImage(); + onResetImage(); } } > { __( 'Reset ' ) } @@ -453,9 +460,7 @@ function BackgroundSizeControls( { const imageValue = style?.background?.backgroundImage?.url || inheritedValue?.background?.backgroundImage?.url; - const isUploadedImage = - style?.background?.backgroundImage?.id || - inheritedValue?.background?.backgroundImage?.id; + const isUploadedImage = style?.background?.backgroundImage?.id; const positionValue = style?.background?.backgroundPosition || inheritedValue?.background?.backgroundPosition; @@ -469,11 +474,19 @@ function BackgroundSizeControls( { * Block-level controls may have different defaults to root-level controls. * A falsy value is treated by default as `auto` (Tile). */ - const currentValueForToggle = + let currentValueForToggle = ! sizeValue && isUploadedImage ? defaultValues?.backgroundSize : sizeValue || 'auto'; - + /* + * The incoming value could be a value + unit, e.g. '20px'. + * In this case set the value to 'tile'. + */ + currentValueForToggle = ! [ 'cover', 'contain', 'auto' ].includes( + currentValueForToggle + ) + ? 'auto' + : currentValueForToggle; /* * If the current value is `cover` and the repeat value is `undefined`, then * the toggle should be unchecked as the default state. Otherwise, the toggle @@ -510,6 +523,7 @@ function BackgroundSizeControls( { * receives a default background position of '50% 0', * when the toggle switches to "Tile". This is to increase the chance that * the image's focus point is visible. + * This is in-editor only to assist with the user experience. */ if ( !! style?.background?.backgroundImage?.id ) { nextPosition = '50% 0'; @@ -562,14 +576,20 @@ function BackgroundSizeControls( { ) ); + // Set a default background position for non-site-wide, uploaded images with a size of 'contain'. + const backgroundPositionValue = + ! positionValue && isUploadedImage && 'contain' === sizeValue + ? defaultValues?.backgroundPosition + : positionValue; + return ( - + hasImageValue } + hasValue={ () => !! value?.background } label={ __( 'Image' ) } onDeselect={ resetBackground } isShownByDefault={ defaultControls.backgroundImage } @@ -749,10 +771,14 @@ export default function BackgroundPanel( { inheritedValue={ inheritedValue } themeFileURIs={ themeFileURIs } displayInPanel - onRemoveImage={ () => { + onResetImage={ () => { setIsDropDownOpen( false ); resetBackground(); } } + onRemoveImage={ () => + setIsDropDownOpen( false ) + } + defaultValues={ defaultValues } /> { + setIsDropDownOpen( false ); + resetBackground(); + } } + onRemoveImage={ () => setIsDropDownOpen( false ) } /> ) } diff --git a/packages/block-editor/src/components/global-styles/test/use-global-styles-output.js b/packages/block-editor/src/components/global-styles/test/use-global-styles-output.js index f648e1db845b87..e2530fdb85f812 100644 --- a/packages/block-editor/src/components/global-styles/test/use-global-styles-output.js +++ b/packages/block-editor/src/components/global-styles/test/use-global-styles-output.js @@ -1061,7 +1061,7 @@ describe( 'global styles renderer', () => { ) ).toEqual( [ "background-image: url( 'https://wordpress.org/assets/image.jpg' )", - 'background-position: center', + 'background-position: 50% 50%', 'background-size: contain', ] ); } ); diff --git a/packages/block-editor/src/hooks/background.js b/packages/block-editor/src/hooks/background.js index cd0b017831b795..0d38068cdefeea 100644 --- a/packages/block-editor/src/hooks/background.js +++ b/packages/block-editor/src/hooks/background.js @@ -23,9 +23,10 @@ import { export const BACKGROUND_SUPPORT_KEY = 'background'; -// Initial control values where no block style is set. -const BACKGROUND_DEFAULT_VALUES = { +// Initial control values. +export const BACKGROUND_BLOCK_DEFAULT_VALUES = { backgroundSize: 'cover', + backgroundPosition: '50% 50%', // used only when backgroundSize is 'contain'. }; /** @@ -55,31 +56,28 @@ export function hasBackgroundSupport( blockName, feature = 'any' ) { } export function setBackgroundStyleDefaults( backgroundStyle ) { - if ( ! backgroundStyle ) { + if ( ! backgroundStyle || ! backgroundStyle?.backgroundImage?.url ) { return; } - const backgroundImage = backgroundStyle?.backgroundImage; let backgroundStylesWithDefaults; // Set block background defaults. - if ( !! backgroundImage?.url ) { - if ( ! backgroundStyle?.backgroundSize ) { - backgroundStylesWithDefaults = { - backgroundSize: 'cover', - }; - } - - if ( - 'contain' === backgroundStyle?.backgroundSize && - ! backgroundStyle?.backgroundPosition - ) { - backgroundStylesWithDefaults = { - backgroundPosition: 'center', - }; - } + if ( ! backgroundStyle?.backgroundSize ) { + backgroundStylesWithDefaults = { + backgroundSize: BACKGROUND_BLOCK_DEFAULT_VALUES.backgroundSize, + }; } + if ( + 'contain' === backgroundStyle?.backgroundSize && + ! backgroundStyle?.backgroundPosition + ) { + backgroundStylesWithDefaults = { + backgroundPosition: + BACKGROUND_BLOCK_DEFAULT_VALUES.backgroundPosition, + }; + } return backgroundStylesWithDefaults; } @@ -147,6 +145,7 @@ export function BackgroundImagePanel( { style: getBlockAttributes( clientId )?.style, _links: _settings[ globalStylesLinksDataKey ], /* + * To ensure we pass down the right inherited values: * @TODO 1. Pass inherited value down to all block style controls, * See: packages/block-editor/src/hooks/style.js * @TODO 2. Add support for block style variations, @@ -187,7 +186,7 @@ export function BackgroundImagePanel( { inheritedValue={ inheritedValue } as={ BackgroundInspectorControl } panelId={ clientId } - defaultValues={ BACKGROUND_DEFAULT_VALUES } + defaultValues={ BACKGROUND_BLOCK_DEFAULT_VALUES } settings={ updatedSettings } onChange={ onChange } value={ style } diff --git a/packages/block-editor/src/hooks/test/background.js b/packages/block-editor/src/hooks/test/background.js new file mode 100644 index 00000000000000..030e88ae67fbd8 --- /dev/null +++ b/packages/block-editor/src/hooks/test/background.js @@ -0,0 +1,60 @@ +/** + * Internal dependencies + */ +import { + setBackgroundStyleDefaults, + BACKGROUND_BLOCK_DEFAULT_VALUES, +} from '../background'; + +describe( 'background', () => { + describe( 'setBackgroundStyleDefaults', () => { + const backgroundStyles = { + backgroundImage: { id: 123, url: 'image.png' }, + }; + const backgroundStylesContain = { + backgroundImage: { id: 123, url: 'image.png' }, + backgroundSize: 'contain', + }; + const backgroundStylesNoURL = { backgroundImage: { id: 123 } }; + it.each( [ + [ + 'return background size default', + backgroundStyles, + { + backgroundSize: + BACKGROUND_BLOCK_DEFAULT_VALUES.backgroundSize, + }, + ], + [ 'return early if no styles are passed', undefined, undefined ], + [ + 'return early if images has no id', + backgroundStylesNoURL, + undefined, + ], + [ + 'return early if images has no URL', + backgroundStylesNoURL, + undefined, + ], + [ + 'return background position default', + backgroundStylesContain, + { + backgroundPosition: + BACKGROUND_BLOCK_DEFAULT_VALUES.backgroundPosition, + }, + ], + [ + 'not apply background position value if one already exists in styles', + { + ...backgroundStylesContain, + backgroundPosition: 'center', + }, + undefined, + ], + ] )( 'should %s', ( message, styles, expected ) => { + const result = setBackgroundStyleDefaults( styles ); + expect( result ).toEqual( expected ); + } ); + } ); +} ); diff --git a/packages/edit-site/src/components/global-styles/screen-block.js b/packages/edit-site/src/components/global-styles/screen-block.js index a16a01956f5a88..dee921f37f1e8a 100644 --- a/packages/edit-site/src/components/global-styles/screen-block.js +++ b/packages/edit-site/src/components/global-styles/screen-block.js @@ -25,9 +25,10 @@ import { VariationsPanel, } from './variations/variations-panel'; -// Initial control values where no block style is set. +// Initial control values. const BACKGROUND_BLOCK_DEFAULT_VALUES = { backgroundSize: 'cover', + backgroundPosition: '50% 50%', // used only when backgroundSize is 'contain'. }; function applyFallbackStyle( border ) { diff --git a/phpunit/block-supports/background-test.php b/phpunit/block-supports/background-test.php index 33e72e9b070106..4c949fa82257d3 100644 --- a/phpunit/block-supports/background-test.php +++ b/phpunit/block-supports/background-test.php @@ -122,7 +122,7 @@ public function test_background_block_support( $theme_name, $block_name, $backgr */ public function data_background_block_support() { return array( - 'background image style is applied' => array( + 'background image style is applied to uploaded images' => array( 'theme_name' => 'block-theme-child-with-fluid-typography', 'block_name' => 'test/background-rules-are-output', 'background_settings' => array( @@ -130,8 +130,8 @@ public function data_background_block_support() { ), 'background_style' => array( 'backgroundImage' => array( - 'url' => 'https://example.com/image.jpg', - 'source' => 'file', + 'url' => 'https://example.com/image.jpg', + 'id' => 123, ), ), 'expected_wrapper' => '
Content
', @@ -157,14 +157,14 @@ public function data_background_block_support() { ), 'background_style' => array( 'backgroundImage' => array( - 'url' => 'https://example.com/image.jpg', - 'source' => 'file', + 'url' => 'https://example.com/image.jpg', + 'id' => 123, ), 'backgroundRepeat' => 'no-repeat', 'backgroundSize' => 'contain', 'backgroundAttachment' => 'fixed', ), - 'expected_wrapper' => '
Content
', + 'expected_wrapper' => '
Content
', 'wrapper' => '
Content
', ), 'background image style is appended if a style attribute already exists' => array( @@ -175,8 +175,8 @@ public function data_background_block_support() { ), 'background_style' => array( 'backgroundImage' => array( - 'url' => 'https://example.com/image.jpg', - 'source' => 'file', + 'url' => 'https://example.com/image.jpg', + 'id' => 123, ), ), 'expected_wrapper' => '
Content
', @@ -190,8 +190,8 @@ public function data_background_block_support() { ), 'background_style' => array( 'backgroundImage' => array( - 'url' => 'https://example.com/image.jpg', - 'source' => 'file', + 'url' => 'https://example.com/image.jpg', + 'id' => 123, ), ), 'expected_wrapper' => '
Content
', @@ -205,8 +205,8 @@ public function data_background_block_support() { ), 'background_style' => array( 'backgroundImage' => array( - 'url' => 'https://example.com/image.jpg', - 'source' => 'file', + 'url' => 'https://example.com/image.jpg', + 'id' => 123, ), ), 'expected_wrapper' => '
Content
', diff --git a/phpunit/class-wp-theme-json-test.php b/phpunit/class-wp-theme-json-test.php index b77b54ecc7d872..d366e0df44c4a2 100644 --- a/phpunit/class-wp-theme-json-test.php +++ b/phpunit/class-wp-theme-json-test.php @@ -4900,7 +4900,7 @@ public function test_get_block_background_image_styles() { ), ); - $quote_styles = ":root :where(.wp-block-quote){background-image: url('http://example.org/quote.png');background-position: center;background-size: contain;}"; + $quote_styles = ":root :where(.wp-block-quote){background-image: url('http://example.org/quote.png');background-position: 50% 50%;background-size: contain;}"; $this->assertSameCSS( $quote_styles, $theme_json->get_styles_for_block( $quote_node ), 'Styles returned from "::get_styles_for_block()" with core/quote default background styles do not match expectations' ); $verse_node = array( From 7c079587eba227e14b09cb34a1cafeeb7b941baa Mon Sep 17 00:00:00 2001 From: Aki Hamano <54422211+t-hamano@users.noreply.github.com> Date: Fri, 9 Aug 2024 10:31:12 +0900 Subject: [PATCH 010/126] Don't allow duplicating template parts in non-block-based themes (#64379) Co-authored-by: t-hamano Co-authored-by: Mamaduka Co-authored-by: MadtownLems --- .../editor/src/components/post-actions/actions.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/editor/src/components/post-actions/actions.js b/packages/editor/src/components/post-actions/actions.js index fcad5b97a13002..425636cefede46 100644 --- a/packages/editor/src/components/post-actions/actions.js +++ b/packages/editor/src/components/post-actions/actions.js @@ -313,9 +313,15 @@ export const duplicateTemplatePartAction = { }; export function usePostActions( { postType, onActionPerformed, context } ) { - const { defaultActions, postTypeObject, userCanCreatePostType } = useSelect( + const { + defaultActions, + postTypeObject, + userCanCreatePostType, + isBlockBasedTheme, + } = useSelect( ( select ) => { - const { getPostType, canUser } = select( coreStore ); + const { getPostType, canUser, getCurrentTheme } = + select( coreStore ); const { getEntityActions } = unlock( select( editorStore ) ); return { postTypeObject: getPostType( postType ), @@ -324,10 +330,12 @@ export function usePostActions( { postType, onActionPerformed, context } ) { kind: 'postType', name: postType, } ), + isBlockBasedTheme: getCurrentTheme()?.is_block_theme, }; }, [ postType ] ); + const { registerPostTypeActions } = unlock( useDispatch( editorStore ) ); useEffect( () => { registerPostTypeActions( postType ); @@ -356,6 +364,7 @@ export function usePostActions( { postType, onActionPerformed, context } ) { : false, isTemplateOrTemplatePart && userCanCreatePostType && + isBlockBasedTheme && duplicateTemplatePartAction, ...defaultActions, ].filter( Boolean ); From b6da9e018677c06a49fc06db1e69aa4bd1460e40 Mon Sep 17 00:00:00 2001 From: Aki Hamano <54422211+t-hamano@users.noreply.github.com> Date: Fri, 9 Aug 2024 10:31:53 +0900 Subject: [PATCH 011/126] Fix: Cancel button in duplicate template part modal doesn't work (#64377) Co-authored-by: t-hamano Co-authored-by: ntsekouras Co-authored-by: Mamaduka --- packages/editor/src/components/post-actions/actions.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/editor/src/components/post-actions/actions.js b/packages/editor/src/components/post-actions/actions.js index 425636cefede46..5e727376a92258 100644 --- a/packages/editor/src/components/post-actions/actions.js +++ b/packages/editor/src/components/post-actions/actions.js @@ -307,6 +307,7 @@ export const duplicateTemplatePartAction = { onCreate={ onTemplatePartSuccess } onError={ closeModal } confirmLabel={ _x( 'Duplicate', 'action label' ) } + closeModal={ closeModal } /> ); }, From 00d28d0e3dcea96b8a1f580bc25192fd197d3567 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Fri, 9 Aug 2024 09:45:01 +0800 Subject: [PATCH 012/126] Grid: Prevent highlight of cells when dragging a block if block type can't be dropped into grid (#64290) Co-authored-by: talldan Co-authored-by: noisysocks Co-authored-by: hanneslsm --- .../src/components/grid/grid-visualizer.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/block-editor/src/components/grid/grid-visualizer.js b/packages/block-editor/src/components/grid/grid-visualizer.js index e1d35f012b4d81..fad2f5cfb14835 100644 --- a/packages/block-editor/src/components/grid/grid-visualizer.js +++ b/packages/block-editor/src/components/grid/grid-visualizer.js @@ -206,8 +206,12 @@ function useGridVisualizerDropZone( gridInfo, setHighlightedRect ) { - const { getBlockAttributes, getBlockRootClientId } = - useSelect( blockEditorStore ); + const { + getBlockAttributes, + getBlockRootClientId, + canInsertBlockType, + getBlockName, + } = useSelect( blockEditorStore ); const { updateBlockAttributes, moveBlocksToPosition, @@ -221,6 +225,10 @@ function useGridVisualizerDropZone( return useDropZoneWithValidation( { validateDrag( srcClientId ) { + const blockName = getBlockName( srcClientId ); + if ( ! canInsertBlockType( blockName, gridClientId ) ) { + return false; + } const attributes = getBlockAttributes( srcClientId ); const rect = new GridRect( { columnStart: column, From b11bdc0645a0503437389c29d39ac266ba6cd2bf Mon Sep 17 00:00:00 2001 From: Sunil Prajapati <61308756+akasunil@users.noreply.github.com> Date: Fri, 9 Aug 2024 09:46:40 +0530 Subject: [PATCH 013/126] Add border support to preformatted block (#64302) Co-authored-by: akasunil Co-authored-by: ramonjd --- packages/block-library/src/preformatted/block.json | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/block-library/src/preformatted/block.json b/packages/block-library/src/preformatted/block.json index fbec3581bc9d42..a1726ee8b0d43c 100644 --- a/packages/block-library/src/preformatted/block.json +++ b/packages/block-library/src/preformatted/block.json @@ -43,6 +43,18 @@ }, "interactivity": { "clientNavigation": true + }, + "__experimentalBorder": { + "radius": true, + "color": true, + "width": true, + "style": true, + "__experimentalDefaultControls": { + "radius": true, + "color": true, + "width": true, + "style": true + } } }, "style": "wp-block-preformatted" From 0ceac8f5e8b74cbeb7ef36c44009c67ec5fabbf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9?= <583546+oandregal@users.noreply.github.com> Date: Fri, 9 Aug 2024 09:16:08 +0200 Subject: [PATCH 014/126] Quick edit: add Date as field and `datetime` as field type (#64267) Co-authored-by: oandregal Co-authored-by: ntsekouras Co-authored-by: youknowriad Co-authored-by: mirka <0mirka00@git.wordpress.org> Co-authored-by: tyxla Co-authored-by: jameskoster --- packages/components/CHANGELOG.md | 4 + .../components/src/date-time/time/index.tsx | 38 +++++--- packages/components/src/date-time/types.ts | 12 ++- .../dataform/stories/index.story.tsx | 18 +++- .../components/dataviews/stories/fixtures.js | 16 ++++ .../dataviews/src/field-types/datetime.tsx | 95 +++++++++++++++++++ packages/dataviews/src/field-types/index.tsx | 5 + .../src/test/filter-and-sort-data-view.js | 28 ++++++ packages/dataviews/src/types.ts | 2 +- .../src/components/post-edit/index.js | 5 +- .../src/components/post-fields/index.js | 1 + 11 files changed, 206 insertions(+), 18 deletions(-) create mode 100644 packages/dataviews/src/field-types/datetime.tsx diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index ef0108b09a741d..47afb0711090d8 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Enhancements + +- `TimePicker`: add `hideLabelFromVision` prop ([#64267](https://github.com/WordPress/gutenberg/pull/64267)). + ## 28.5.0 (2024-08-07) ### Bug Fixes diff --git a/packages/components/src/date-time/time/index.tsx b/packages/components/src/date-time/time/index.tsx index 5f706d69190095..90a8a901354a39 100644 --- a/packages/components/src/date-time/time/index.tsx +++ b/packages/components/src/date-time/time/index.tsx @@ -13,6 +13,7 @@ import { __ } from '@wordpress/i18n'; * Internal dependencies */ import BaseControl from '../../base-control'; +import { VisuallyHidden } from '../../visually-hidden'; import SelectControl from '../../select-control'; import TimeZone from './timezone'; import type { TimeInputValue, TimePickerProps } from '../types'; @@ -61,6 +62,7 @@ export function TimePicker( { currentTime, onChange, dateOrder: dateOrderProp, + hideLabelFromVision = false, }: TimePickerProps ) { const [ date, setDate ] = useState( () => // Truncate the date at the minutes, see: #15495. @@ -219,12 +221,18 @@ export function TimePicker( { className="components-datetime__time" // Unused, for backwards compatibility. >
- - { __( 'Time' ) } - + { hideLabelFromVision ? ( + + { __( 'Time' ) } + + ) : ( + + { __( 'Time' ) } + + ) } @@ -241,12 +249,18 @@ export function TimePicker( {
- - { __( 'Date' ) } - + { hideLabelFromVision ? ( + + { __( 'Date' ) } + + ) : ( + + { __( 'Date' ) } + + ) } diff --git a/packages/components/src/date-time/types.ts b/packages/components/src/date-time/types.ts index b3716d5c135c64..1f2298d9d450b5 100644 --- a/packages/components/src/date-time/types.ts +++ b/packages/components/src/date-time/types.ts @@ -29,6 +29,13 @@ export type TimePickerProps = { * time as an argument. */ onChange?: ( time: string ) => void; + + /** + * If true, the label will only be visible to screen readers. + * + * @default false + */ + hideLabelFromVision?: boolean; }; export type TimeInputValue = { @@ -130,7 +137,10 @@ export type DatePickerProps = { }; export type DateTimePickerProps = Omit< DatePickerProps, 'onChange' > & - Omit< TimePickerProps, 'currentTime' | 'onChange' > & { + Omit< + TimePickerProps, + 'currentTime' | 'onChange' | 'hideLabelFromVision' + > & { /** * The function called when a new date or time has been selected. It is * passed the date and time as an argument. diff --git a/packages/dataviews/src/components/dataform/stories/index.story.tsx b/packages/dataviews/src/components/dataform/stories/index.story.tsx index 4863eb24b4ede4..7808756b7b6557 100644 --- a/packages/dataviews/src/components/dataform/stories/index.story.tsx +++ b/packages/dataviews/src/components/dataform/stories/index.story.tsx @@ -33,6 +33,20 @@ const fields = [ label: 'Order', type: 'integer' as const, }, + { + id: 'date', + label: 'Date', + type: 'datetime' as const, + }, + { + id: 'birthdate', + label: 'Date as options', + type: 'datetime' as const, + elements: [ + { value: '1970-02-23T12:00:00', label: "Jane's birth date" }, + { value: '1950-02-23T12:00:00', label: "John's birth date" }, + ], + }, { id: 'author', label: 'Author', @@ -59,10 +73,12 @@ export const Default = ( { type }: { type: 'panel' | 'regular' } ) => { order: 2, author: 1, status: 'draft', + date: '2021-01-01T12:00:00', + birthdate: '1950-02-23T12:00:00', } ); const form = { - fields: [ 'title', 'order', 'author', 'status' ], + fields: [ 'title', 'order', 'author', 'status', 'date', 'birthdate' ], }; return ( diff --git a/packages/dataviews/src/components/dataviews/stories/fixtures.js b/packages/dataviews/src/components/dataviews/stories/fixtures.js index 536c5e66e6ce97..f89cea81e6f15d 100644 --- a/packages/dataviews/src/components/dataviews/stories/fixtures.js +++ b/packages/dataviews/src/components/dataviews/stories/fixtures.js @@ -23,6 +23,7 @@ export const data = [ type: 'Not a planet', categories: [ 'Space', 'NASA' ], satellites: 0, + date: '2021-01-01T00:00:00Z', }, { id: 2, @@ -32,6 +33,7 @@ export const data = [ type: 'Not a planet', categories: [ 'Space' ], satellites: 0, + date: '2019-01-02T00:00:00Z', }, { id: 3, @@ -41,6 +43,7 @@ export const data = [ type: 'Not a planet', categories: [ 'NASA' ], satellites: 0, + date: '2025-01-03T00:00:00Z', }, { id: 4, @@ -50,6 +53,7 @@ export const data = [ type: 'Ice giant', categories: [ 'Space', 'Planet', 'Solar system' ], satellites: 14, + date: '2020-01-01T00:00:00Z', }, { id: 5, @@ -59,6 +63,7 @@ export const data = [ type: 'Terrestrial', categories: [ 'Space', 'Planet', 'Solar system' ], satellites: 0, + date: '2020-01-02T01:00:00Z', }, { id: 6, @@ -68,6 +73,7 @@ export const data = [ type: 'Terrestrial', categories: [ 'Space', 'Planet', 'Solar system' ], satellites: 0, + date: '2020-01-02T00:00:00Z', }, { id: 7, @@ -77,6 +83,7 @@ export const data = [ type: 'Terrestrial', categories: [ 'Space', 'Planet', 'Solar system' ], satellites: 1, + date: '2023-01-03T00:00:00Z', }, { id: 8, @@ -86,6 +93,7 @@ export const data = [ type: 'Terrestrial', categories: [ 'Space', 'Planet', 'Solar system' ], satellites: 2, + date: '2020-01-01T00:00:00Z', }, { id: 9, @@ -95,6 +103,7 @@ export const data = [ type: 'Gas giant', categories: [ 'Space', 'Planet', 'Solar system' ], satellites: 95, + date: '2017-01-01T00:01:00Z', }, { id: 10, @@ -104,6 +113,7 @@ export const data = [ type: 'Gas giant', categories: [ 'Space', 'Planet', 'Solar system' ], satellites: 146, + date: '2020-02-01T00:02:00Z', }, { id: 11, @@ -113,6 +123,7 @@ export const data = [ type: 'Ice giant', categories: [ 'Space', 'Ice giant', 'Solar system' ], satellites: 28, + date: '2020-03-01T00:00:00Z', }, ]; @@ -175,6 +186,11 @@ export const fields = [ enableHiding: false, enableGlobalSearch: true, }, + { + id: 'date', + label: 'Date', + type: 'datetime', + }, { label: 'Type', id: 'type', diff --git a/packages/dataviews/src/field-types/datetime.tsx b/packages/dataviews/src/field-types/datetime.tsx new file mode 100644 index 00000000000000..3861a0db635d7b --- /dev/null +++ b/packages/dataviews/src/field-types/datetime.tsx @@ -0,0 +1,95 @@ +/** + * WordPress dependencies + */ +import { BaseControl, TimePicker, SelectControl } from '@wordpress/components'; +import { useCallback } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import type { + SortDirection, + ValidationContext, + DataFormControlProps, +} from '../types'; + +function sort( a: any, b: any, direction: SortDirection ) { + const timeA = new Date( a ).getTime(); + const timeB = new Date( b ).getTime(); + + return direction === 'asc' ? timeA - timeB : timeB - timeA; +} + +function isValid( value: any, context?: ValidationContext ) { + if ( context?.elements ) { + const validValues = context?.elements.map( ( f ) => f.value ); + if ( ! validValues.includes( value ) ) { + return false; + } + } + + return true; +} + +function Edit< Item >( { + data, + field, + onChange, +}: DataFormControlProps< Item > ) { + const { id, label } = field; + const value = field.getValue( { item: data } ); + + const onChangeControl = useCallback( + ( newValue: string | null ) => + onChange( ( prevItem: Item ) => ( { + ...prevItem, + [ id ]: newValue, + } ) ), + [ id, onChange ] + ); + + if ( field.elements ) { + const elements = [ + /* + * Value can be undefined when: + * + * - the field is not required + * - in bulk editing + * + */ + { label: __( 'Select item' ), value: '' }, + ...field.elements, + ]; + + return ( + + ); + } + + return ( +
+ + { label } + + +
+ ); +} + +export default { + sort, + isValid, + Edit, +}; diff --git a/packages/dataviews/src/field-types/index.tsx b/packages/dataviews/src/field-types/index.tsx index 3d1d824ec36bf2..eb9dada479c6bf 100644 --- a/packages/dataviews/src/field-types/index.tsx +++ b/packages/dataviews/src/field-types/index.tsx @@ -4,6 +4,7 @@ import type { FieldType, SortDirection, ValidationContext } from '../types'; import { default as integer } from './integer'; import { default as text } from './text'; +import { default as datetime } from './datetime'; /** * @@ -20,6 +21,10 @@ export default function getFieldTypeDefinition( type?: FieldType ) { return text; } + if ( 'datetime' === type ) { + return datetime; + } + return { sort: ( a: any, b: any, direction: SortDirection ) => { if ( typeof a === 'number' && typeof b === 'number' ) { diff --git a/packages/dataviews/src/test/filter-and-sort-data-view.js b/packages/dataviews/src/test/filter-and-sort-data-view.js index 7538b975a30992..7f0b3a30e8587c 100644 --- a/packages/dataviews/src/test/filter-and-sort-data-view.js +++ b/packages/dataviews/src/test/filter-and-sort-data-view.js @@ -268,6 +268,34 @@ describe( 'sorting', () => { expect( result[ 1 ].title ).toBe( 'Neptune' ); } ); + it( 'should sort datetime field types', () => { + const { data: resultDesc } = filterSortAndPaginate( + data, + { + sort: { field: 'date', direction: 'desc' }, + }, + fields + ); + expect( resultDesc ).toHaveLength( 11 ); + expect( resultDesc[ 0 ].title ).toBe( 'NASA' ); + expect( resultDesc[ 1 ].title ).toBe( 'Earth' ); + expect( resultDesc[ 9 ].title ).toBe( 'Space' ); + expect( resultDesc[ 10 ].title ).toBe( 'Jupiter' ); + + const { data: resultAsc } = filterSortAndPaginate( + data, + { + sort: { field: 'date', direction: 'asc' }, + }, + fields + ); + expect( resultAsc ).toHaveLength( 11 ); + expect( resultAsc[ 0 ].title ).toBe( 'Jupiter' ); + expect( resultAsc[ 1 ].title ).toBe( 'Space' ); + expect( resultAsc[ 9 ].title ).toBe( 'Earth' ); + expect( resultAsc[ 10 ].title ).toBe( 'NASA' ); + } ); + it( 'should sort untyped fields if the value is a number', () => { const { data: result } = filterSortAndPaginate( data, diff --git a/packages/dataviews/src/types.ts b/packages/dataviews/src/types.ts index b0873e9c677f53..2e87d1371acb61 100644 --- a/packages/dataviews/src/types.ts +++ b/packages/dataviews/src/types.ts @@ -47,7 +47,7 @@ export type Operator = | 'isAll' | 'isNotAll'; -export type FieldType = 'text' | 'integer'; +export type FieldType = 'text' | 'integer' | 'datetime'; export type ValidationContext = { elements?: Option[]; diff --git a/packages/edit-site/src/components/post-edit/index.js b/packages/edit-site/src/components/post-edit/index.js index 86e2bf4d6edea1..03158e00862154 100644 --- a/packages/edit-site/src/components/post-edit/index.js +++ b/packages/edit-site/src/components/post-edit/index.js @@ -47,9 +47,9 @@ function PostEditForm( { postType, postId } ) { const { fields } = usePostFields(); const form = { type: 'panel', - fields: [ 'title', 'author' ], + fields: [ 'title', 'author', 'date' ], }; - const [ edits, setEdits ] = useState( {} ); + const [ edits, setEdits ] = useState( initialEdits ); const itemWithEdits = useMemo( () => { return { ...initialEdits, @@ -71,7 +71,6 @@ function PostEditForm( { postType, postId } ) { ...edits, } ); } - setEdits( {} ); }; const isUpdateDisabled = ! isItemValid( itemWithEdits, fields, form ); diff --git a/packages/edit-site/src/components/post-fields/index.js b/packages/edit-site/src/components/post-fields/index.js index f476b676264ed8..44625fbfbfafb9 100644 --- a/packages/edit-site/src/components/post-fields/index.js +++ b/packages/edit-site/src/components/post-fields/index.js @@ -271,6 +271,7 @@ function usePostFields( viewType ) { { label: __( 'Date' ), id: 'date', + type: 'datetime', render: ( { item } ) => { const isDraftOrPrivate = [ 'draft', 'private' ].includes( item.status From 97285c23d69955776b7d1b6d1468167d081beb7b Mon Sep 17 00:00:00 2001 From: Shail Mehta Date: Fri, 9 Aug 2024 13:20:15 +0530 Subject: [PATCH 015/126] Comment Date: Added Border Support (#64210) Co-authored-by: shail-mehta Co-authored-by: t-hamano Co-authored-by: aaronrobertshaw --- .../block-library/src/comment-date/block.json | 15 ++++++++++++++- .../block-library/src/comment-date/style.scss | 4 ++++ packages/block-library/src/style.scss | 1 + 3 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 packages/block-library/src/comment-date/style.scss diff --git a/packages/block-library/src/comment-date/block.json b/packages/block-library/src/comment-date/block.json index ddc0281e6c668c..1e8110f836d93b 100644 --- a/packages/block-library/src/comment-date/block.json +++ b/packages/block-library/src/comment-date/block.json @@ -47,6 +47,19 @@ }, "interactivity": { "clientNavigation": true + }, + "__experimentalBorder": { + "radius": true, + "color": true, + "width": true, + "style": true, + "__experimentalDefaultControls": { + "radius": true, + "color": true, + "width": true, + "style": true + } } - } + }, + "style": "wp-block-comment-date" } diff --git a/packages/block-library/src/comment-date/style.scss b/packages/block-library/src/comment-date/style.scss new file mode 100644 index 00000000000000..9b897defa655b7 --- /dev/null +++ b/packages/block-library/src/comment-date/style.scss @@ -0,0 +1,4 @@ +.wp-block-comment-date { + // This block has customizable padding, border-box makes that more predictable. + box-sizing: border-box; +} diff --git a/packages/block-library/src/style.scss b/packages/block-library/src/style.scss index 8f17cd7a50f55c..df1337d7eb25c0 100644 --- a/packages/block-library/src/style.scss +++ b/packages/block-library/src/style.scss @@ -10,6 +10,7 @@ @import "./comments/style.scss"; @import "./comments-pagination/style.scss"; @import "./comment-template/style.scss"; +@import "./comment-date/style.scss"; @import "./cover/style.scss"; @import "./details/style.scss"; @import "./embed/style.scss"; From 651cc457521a6ec81367362618433095ae611fb0 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Fri, 9 Aug 2024 10:49:31 +0200 Subject: [PATCH 016/126] DataViews Quick Edit: Add Post Card to the quick edit panel (#64365) Co-authored-by: youknowriad Co-authored-by: oandregal Co-authored-by: jameskoster --- .../src/components/post-edit/index.js | 7 ++++ .../src/components/post-card-panel/index.js | 42 ++++++++----------- .../src/components/sidebar/post-summary.js | 29 ++++++++----- packages/editor/src/private-apis.js | 2 + 4 files changed, 45 insertions(+), 35 deletions(-) diff --git a/packages/edit-site/src/components/post-edit/index.js b/packages/edit-site/src/components/post-edit/index.js index 03158e00862154..35141f743cd2e3 100644 --- a/packages/edit-site/src/components/post-edit/index.js +++ b/packages/edit-site/src/components/post-edit/index.js @@ -16,12 +16,16 @@ import { __experimentalVStack as VStack, } from '@wordpress/components'; import { useState, useMemo } from '@wordpress/element'; +import { privateApis as editorPrivateApis } from '@wordpress/editor'; /** * Internal dependencies */ import Page from '../page'; import usePostFields from '../post-fields'; +import { unlock } from '../../lock-unlock'; + +const { PostCardPanel } = unlock( editorPrivateApis ); function PostEditForm( { postType, postId } ) { const ids = useMemo( () => postId.split( ',' ), [ postId ] ); @@ -76,6 +80,9 @@ function PostEditForm( { postType, postId } ) { const isUpdateDisabled = ! isItemValid( itemWithEdits, fields, form ); return ( + { ids.length === 1 && ( + + ) } { - const { - getEditedPostAttribute, - getCurrentPostType, - getCurrentPostId, - __experimentalGetTemplateInfo, - } = select( editorStore ); - const { canUser } = select( coreStore ); - const { getEditedEntityRecord } = select( coreStore ); + const { __experimentalGetTemplateInfo } = select( editorStore ); + const { canUser, getEditedEntityRecord } = select( coreStore ); const siteSettings = canUser( 'read', { kind: 'root', name: 'site', } ) ? getEditedEntityRecord( 'root', 'site' ) : undefined; - const _type = getCurrentPostType(); - const _id = getCurrentPostId(); - const _record = getEditedEntityRecord( 'postType', _type, _id ); + const _record = getEditedEntityRecord( + 'postType', + postType, + postId + ); const _templateInfo = [ TEMPLATE_POST_TYPE, TEMPLATE_PART_POST_TYPE ].includes( - _type + postType ) && __experimentalGetTemplateInfo( _record ); let _isSync = false; - if ( GLOBAL_POST_TYPES.includes( _type ) ) { - if ( PATTERN_POST_TYPE === _type ) { + if ( GLOBAL_POST_TYPES.includes( postType ) ) { + if ( PATTERN_POST_TYPE === postType ) { // When the post is first created, the top level wp_pattern_sync_status is not set so get meta value instead. const currentSyncStatus = - getEditedPostAttribute( 'meta' ) - ?.wp_pattern_sync_status === 'unsynced' + _record?.meta?.wp_pattern_sync_status === 'unsynced' ? 'unsynced' - : getEditedPostAttribute( - 'wp_pattern_sync_status' - ); + : _record?.wp_pattern_sync_status; _isSync = currentSyncStatus !== 'unsynced'; } else { _isSync = true; } } return { - title: - _templateInfo?.title || getEditedPostAttribute( 'title' ), - icon: unlock( select( editorStore ) ).getPostIcon( _type, { + title: _templateInfo?.title || _record?.title, + icon: unlock( select( editorStore ) ).getPostIcon( postType, { area: _record?.area, } ), isSync: _isSync, - isFrontPage: siteSettings?.page_on_front === _id, - isPostsPage: siteSettings?.page_for_posts === _id, + isFrontPage: siteSettings?.page_on_front === postId, + isPostsPage: siteSettings?.page_for_posts === postId, }; }, [] diff --git a/packages/editor/src/components/sidebar/post-summary.js b/packages/editor/src/components/sidebar/post-summary.js index a6a95d36388ba5..573a59de827188 100644 --- a/packages/editor/src/components/sidebar/post-summary.js +++ b/packages/editor/src/components/sidebar/post-summary.js @@ -36,16 +36,23 @@ import { PrivatePostLastRevision } from '../post-last-revision'; const PANEL_NAME = 'post-status'; export default function PostSummary( { onActionPerformed } ) { - const { isRemovedPostStatusPanel } = useSelect( ( select ) => { - // We use isEditorPanelRemoved to hide the panel if it was programatically removed. We do - // not use isEditorPanelEnabled since this panel should not be disabled through the UI. - const { isEditorPanelRemoved, getCurrentPostType } = - select( editorStore ); - return { - isRemovedPostStatusPanel: isEditorPanelRemoved( PANEL_NAME ), - postType: getCurrentPostType(), - }; - }, [] ); + const { isRemovedPostStatusPanel, postType, postId } = useSelect( + ( select ) => { + // We use isEditorPanelRemoved to hide the panel if it was programatically removed. We do + // not use isEditorPanelEnabled since this panel should not be disabled through the UI. + const { + isEditorPanelRemoved, + getCurrentPostType, + getCurrentPostId, + } = select( editorStore ); + return { + isRemovedPostStatusPanel: isEditorPanelRemoved( PANEL_NAME ), + postType: getCurrentPostType(), + postId: getCurrentPostId(), + }; + }, + [] + ); return ( @@ -54,6 +61,8 @@ export default function PostSummary( { onActionPerformed } ) { <> Date: Fri, 9 Aug 2024 10:57:41 +0200 Subject: [PATCH 017/126] Add `comment_status` field to quick edit (#64370) Co-authored-by: oandregal Co-authored-by: youknowriad Co-authored-by: ntsekouras --- .../components/dataform-controls/index.tsx | 46 +++++++++++++++++++ .../components/dataform-controls/radio.tsx | 43 +++++++++++++++++ .../dataform/stories/index.story.tsx | 22 ++++++++- packages/dataviews/src/normalize-fields.ts | 3 +- packages/dataviews/src/types.ts | 22 ++++++++- .../src/components/post-edit/index.js | 2 +- .../src/components/post-fields/index.js | 26 +++++++++++ 7 files changed, 160 insertions(+), 4 deletions(-) create mode 100644 packages/dataviews/src/components/dataform-controls/index.tsx create mode 100644 packages/dataviews/src/components/dataform-controls/radio.tsx diff --git a/packages/dataviews/src/components/dataform-controls/index.tsx b/packages/dataviews/src/components/dataform-controls/index.tsx new file mode 100644 index 00000000000000..dd913269cd09ea --- /dev/null +++ b/packages/dataviews/src/components/dataform-controls/index.tsx @@ -0,0 +1,46 @@ +/** + * External dependencies + */ +import type { ComponentType } from 'react'; + +/** + * Internal dependencies + */ +import type { + DataFormControlProps, + Field, + FieldTypeDefinition, +} from '../../types'; +import radio from './radio'; + +interface FormControls { + [ key: string ]: ComponentType< DataFormControlProps< any > >; +} + +const FORM_CONTROLS: FormControls = { + radio, +}; + +export function getControl< Item >( + field: Field< Item >, + fieldTypeDefinition: FieldTypeDefinition< Item > +) { + if ( typeof field.Edit === 'function' ) { + return field.Edit; + } + + let control; + if ( typeof field.Edit === 'string' ) { + control = getControlByType( field.Edit ); + } + + return control || fieldTypeDefinition.Edit; +} + +export function getControlByType( type: string ) { + if ( Object.keys( FORM_CONTROLS ).includes( type ) ) { + return FORM_CONTROLS[ type ]; + } + + return null; +} diff --git a/packages/dataviews/src/components/dataform-controls/radio.tsx b/packages/dataviews/src/components/dataform-controls/radio.tsx new file mode 100644 index 00000000000000..d264aa6c24b7fb --- /dev/null +++ b/packages/dataviews/src/components/dataform-controls/radio.tsx @@ -0,0 +1,43 @@ +/** + * WordPress dependencies + */ +import { RadioControl } from '@wordpress/components'; +import { useCallback } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import type { DataFormControlProps } from '../../types'; + +export default function Edit< Item >( { + data, + field, + onChange, + hideLabelFromVision, +}: DataFormControlProps< Item > ) { + const { id, label } = field; + const value = field.getValue( { item: data } ); + + const onChangeControl = useCallback( + ( newValue: string ) => + onChange( ( prevItem: Item ) => ( { + ...prevItem, + [ id ]: newValue, + } ) ), + [ id, onChange ] + ); + + if ( field.elements ) { + return ( + + ); + } + + return null; +} diff --git a/packages/dataviews/src/components/dataform/stories/index.story.tsx b/packages/dataviews/src/components/dataform/stories/index.story.tsx index 7808756b7b6557..7f3c5ff879b72a 100644 --- a/packages/dataviews/src/components/dataform/stories/index.story.tsx +++ b/packages/dataviews/src/components/dataform/stories/index.story.tsx @@ -56,6 +56,17 @@ const fields = [ { value: 2, label: 'John' }, ], }, + { + id: 'reviewer', + label: 'Reviewer', + type: 'text' as const, + Edit: 'radio' as const, + elements: [ + { value: 'fulano', label: 'Fulano' }, + { value: 'mengano', label: 'Mengano' }, + { value: 'zutano', label: 'Zutano' }, + ], + }, { id: 'status', label: 'Status', @@ -73,12 +84,21 @@ export const Default = ( { type }: { type: 'panel' | 'regular' } ) => { order: 2, author: 1, status: 'draft', + reviewer: 'fulano', date: '2021-01-01T12:00:00', birthdate: '1950-02-23T12:00:00', } ); const form = { - fields: [ 'title', 'order', 'author', 'status', 'date', 'birthdate' ], + fields: [ + 'title', + 'order', + 'author', + 'reviewer', + 'status', + 'date', + 'birthdate', + ], }; return ( diff --git a/packages/dataviews/src/normalize-fields.ts b/packages/dataviews/src/normalize-fields.ts index 680749df5344a6..54992ff22fe2ae 100644 --- a/packages/dataviews/src/normalize-fields.ts +++ b/packages/dataviews/src/normalize-fields.ts @@ -3,6 +3,7 @@ */ import getFieldTypeDefinition from './field-types'; import type { Field, NormalizedField } from './types'; +import { getControl } from './components/dataform-controls'; /** * Apply default values and normalize the fields config. @@ -38,7 +39,7 @@ export function normalizeFields< Item >( ); }; - const Edit = field.Edit || fieldTypeDefinition.Edit; + const Edit = getControl( field, fieldTypeDefinition ); const renderFromElements = ( { item }: { item: Item } ) => { const value = getValue( { item } ); diff --git a/packages/dataviews/src/types.ts b/packages/dataviews/src/types.ts index 2e87d1371acb61..d8a5ee8f68ecef 100644 --- a/packages/dataviews/src/types.ts +++ b/packages/dataviews/src/types.ts @@ -53,6 +53,26 @@ export type ValidationContext = { elements?: Option[]; }; +/** + * An abstract interface for Field based on the field type. + */ +export type FieldTypeDefinition< Item > = { + /** + * Callback used to sort the field. + */ + sort: ( a: Item, b: Item, direction: SortDirection ) => number; + + /** + * Callback used to validate the field. + */ + isValid: ( item: Item, context?: ValidationContext ) => boolean; + + /** + * Callback used to render an edit control for the field. + */ + Edit: ComponentType< DataFormControlProps< Item > >; +}; + /** * A dataview field for a specific property of a data type. */ @@ -90,7 +110,7 @@ export type Field< Item > = { /** * Callback used to render an edit control for the field. */ - Edit?: ComponentType< DataFormControlProps< Item > >; + Edit?: ComponentType< DataFormControlProps< Item > > | 'radio'; /** * Callback used to sort the field. diff --git a/packages/edit-site/src/components/post-edit/index.js b/packages/edit-site/src/components/post-edit/index.js index 35141f743cd2e3..0a56fdfe5786b7 100644 --- a/packages/edit-site/src/components/post-edit/index.js +++ b/packages/edit-site/src/components/post-edit/index.js @@ -51,7 +51,7 @@ function PostEditForm( { postType, postId } ) { const { fields } = usePostFields(); const form = { type: 'panel', - fields: [ 'title', 'author', 'date' ], + fields: [ 'title', 'author', 'date', 'comment_status' ], }; const [ edits, setEdits ] = useState( initialEdits ); const itemWithEdits = useMemo( () => { diff --git a/packages/edit-site/src/components/post-fields/index.js b/packages/edit-site/src/components/post-fields/index.js index 44625fbfbfafb9..b03b2c6f5be3c4 100644 --- a/packages/edit-site/src/components/post-fields/index.js +++ b/packages/edit-site/src/components/post-fields/index.js @@ -345,6 +345,32 @@ function usePostFields( viewType ) { return ; }, }, + { + id: 'comment_status', + label: __( 'Discussion' ), + type: 'text', + Edit: 'radio', + enableSorting: false, + filterBy: { + operators: [], + }, + elements: [ + { + value: 'open', + label: __( 'Open' ), + description: __( + 'Visitors can add new comments and replies.' + ), + }, + { + value: 'closed', + label: __( 'Closed' ), + description: __( + 'Visitors cannot add new comments or replies. Existing comments remain visible.' + ), + }, + ], + }, ], [ authors, viewType, frontPageId, postsPageId ] ); From 360c6f9b2be933baa0c224ce7a2d4f48593bee0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9?= <583546+oandregal@users.noreply.github.com> Date: Fri, 9 Aug 2024 11:42:34 +0200 Subject: [PATCH 018/126] DataViews: update `renderFormElements` to make sure the value respects the type (#64391) Co-authored-by: oandregal Co-authored-by: youknowriad --- packages/dataviews/src/field-types/integer.tsx | 2 +- packages/dataviews/src/normalize-fields.ts | 11 ++++------- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/packages/dataviews/src/field-types/integer.tsx b/packages/dataviews/src/field-types/integer.tsx index 1233c3ab555567..bd4956493232df 100644 --- a/packages/dataviews/src/field-types/integer.tsx +++ b/packages/dataviews/src/field-types/integer.tsx @@ -53,7 +53,7 @@ function Edit< Item >( { ( newValue: string | undefined ) => onChange( ( prevItem: Item ) => ( { ...prevItem, - [ id ]: newValue, + [ id ]: Number( newValue ), } ) ), [ id, onChange ] ); diff --git a/packages/dataviews/src/normalize-fields.ts b/packages/dataviews/src/normalize-fields.ts index 54992ff22fe2ae..2cdde5b3343798 100644 --- a/packages/dataviews/src/normalize-fields.ts +++ b/packages/dataviews/src/normalize-fields.ts @@ -43,13 +43,10 @@ export function normalizeFields< Item >( const renderFromElements = ( { item }: { item: Item } ) => { const value = getValue( { item } ); - const label = field?.elements?.find( ( element ) => { - // Intentionally using == here to allow for type coercion. - // eslint-disable-next-line eqeqeq - return element.value == value; - } )?.label; - - return label || value; + return ( + field?.elements?.find( ( element ) => element.value === value ) + ?.label || getValue( { item } ) + ); }; const render = From 758915a0e4346ca51a9e3fda5bee4500ff4e3976 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Fri, 9 Aug 2024 11:49:04 +0200 Subject: [PATCH 019/126] DataViews Quick Edit: Fix panel title not updating --- packages/editor/src/components/post-card-panel/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/editor/src/components/post-card-panel/index.js b/packages/editor/src/components/post-card-panel/index.js index 5e20ecbcf07df9..5ac88b3dbc7f94 100644 --- a/packages/editor/src/components/post-card-panel/index.js +++ b/packages/editor/src/components/post-card-panel/index.js @@ -70,7 +70,7 @@ export default function PostCardPanel( { postType, postId, actions } ) { isPostsPage: siteSettings?.page_for_posts === postId, }; }, - [] + [ postId, postType ] ); return (
From f5959ccdbab3fab3f95525c2d711e419cffe7d0a Mon Sep 17 00:00:00 2001 From: Amit Raj <77401999+amitraj2203@users.noreply.github.com> Date: Fri, 9 Aug 2024 15:39:42 +0530 Subject: [PATCH 020/126] Latests Posts: Used ToggleGroupControl instead for Image alignment (#64352) * Used ToggleGroupControl instead of BlockAlignmentToolbar * Addressed feedback * Use position icons instead of align * Remove isBlock prop from ToggleGroupControl Co-authored-by: amitraj2203 Co-authored-by: mirka <0mirka00@git.wordpress.org> Co-authored-by: t-hamano --- .../block-library/src/latest-posts/edit.js | 76 ++++++++++++++----- .../src/latest-posts/editor.scss | 6 -- 2 files changed, 59 insertions(+), 23 deletions(-) diff --git a/packages/block-library/src/latest-posts/edit.js b/packages/block-library/src/latest-posts/edit.js index 49a24b08f68d77..533e9621fd4f2b 100644 --- a/packages/block-library/src/latest-posts/edit.js +++ b/packages/block-library/src/latest-posts/edit.js @@ -7,7 +7,6 @@ import clsx from 'clsx'; * WordPress dependencies */ import { - BaseControl, PanelBody, Placeholder, QueryControls, @@ -16,19 +15,28 @@ import { Spinner, ToggleControl, ToolbarGroup, + __experimentalToggleGroupControl as ToggleGroupControl, + __experimentalToggleGroupControlOptionIcon as ToggleGroupControlOptionIcon, } from '@wordpress/components'; import { __, _x, sprintf } from '@wordpress/i18n'; import { dateI18n, format, getSettings } from '@wordpress/date'; import { InspectorControls, - BlockAlignmentToolbar, BlockControls, __experimentalImageSizeControl as ImageSizeControl, useBlockProps, store as blockEditorStore, } from '@wordpress/block-editor'; import { useSelect, useDispatch } from '@wordpress/data'; -import { pin, list, grid } from '@wordpress/icons'; +import { + pin, + list, + grid, + alignNone, + positionLeft, + positionCenter, + positionRight, +} from '@wordpress/icons'; import { store as coreStore } from '@wordpress/core-data'; import { store as noticeStore } from '@wordpress/notices'; import { useInstanceId } from '@wordpress/compose'; @@ -197,6 +205,29 @@ export default function LatestPostsEdit( { attributes, setAttributes } ) { setAttributes( { categories: allCategories } ); }; + const imageAlignmentOptions = [ + { + value: 'none', + icon: alignNone, + label: __( 'None' ), + }, + { + value: 'left', + icon: positionLeft, + label: __( 'Left' ), + }, + { + value: 'center', + icon: positionCenter, + label: __( 'Center' ), + }, + { + value: 'right', + icon: positionRight, + label: __( 'Right' ), + }, + ]; + const hasPosts = !! latestPosts?.length; const inspectorControls = ( @@ -303,21 +334,32 @@ export default function LatestPostsEdit( { attributes, setAttributes } ) { } ) } /> - - - { __( 'Image alignment' ) } - - - setAttributes( { - featuredImageAlign: value, - } ) + + setAttributes( { + featuredImageAlign: + value !== 'none' ? value : undefined, + } ) + } + > + { imageAlignmentOptions.map( + ( { value, icon, label } ) => { + return ( + + ); } - controls={ [ 'left', 'center', 'right' ] } - isCollapsed={ false } - /> - + ) } + Date: Fri, 9 Aug 2024 19:13:06 +0900 Subject: [PATCH 021/126] Post Editor: Force iframe editor when zoom-out mode (#64316) Co-authored-by: t-hamano Co-authored-by: stokesman --- .../edit-post/src/components/layout/use-should-iframe.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/edit-post/src/components/layout/use-should-iframe.js b/packages/edit-post/src/components/layout/use-should-iframe.js index 03efae92c72f7d..248ea53109f250 100644 --- a/packages/edit-post/src/components/layout/use-should-iframe.js +++ b/packages/edit-post/src/components/layout/use-should-iframe.js @@ -4,6 +4,7 @@ import { store as editorStore } from '@wordpress/editor'; import { useSelect } from '@wordpress/data'; import { store as blocksStore } from '@wordpress/blocks'; +import { store as blockEditorStore } from '@wordpress/block-editor'; /** * Internal dependencies @@ -18,8 +19,10 @@ export function useShouldIframe() { hasV3BlocksOnly, isEditingTemplate, hasMetaBoxes, + isZoomOutMode, } = useSelect( ( select ) => { const { getEditorSettings, getCurrentPostType } = select( editorStore ); + const { __unstableGetEditorMode } = select( blockEditorStore ); const { getBlockTypes } = select( blocksStore ); const editorSettings = getEditorSettings(); return { @@ -29,12 +32,14 @@ export function useShouldIframe() { } ), isEditingTemplate: getCurrentPostType() === 'wp_template', hasMetaBoxes: select( editPostStore ).hasMetaBoxes(), + isZoomOutMode: __unstableGetEditorMode() === 'zoom-out', }; }, [] ); return ( ( ( hasV3BlocksOnly || ( isGutenbergPlugin && isBlockBasedTheme ) ) && ! hasMetaBoxes ) || - isEditingTemplate + isEditingTemplate || + isZoomOutMode ); } From 22e4cc2e24e89901d7b0b8099a7ecc4f695e04e2 Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Fri, 9 Aug 2024 12:18:49 +0200 Subject: [PATCH 022/126] Composite: stabilize new ariakit implementation (#63564) * Point legacy exports directly to the source (instead of folder root) * Swap default folder export to new version * Apply compound component naming * Export new version from the package * Update (fix) private APIs exports * Update composite implementation to use new compound naming * Update references to Composite inside components package * Update Storybook entry points for both legacy and current * Fix Storybook generated docs * Add todo * Remove unncecessary code * CHANGELOG * README * Add JSDocs to Composite exports * Move current implementation out of `current` folder * Fix import in the legacy implementation * Update docs manifest * Fix type in Storybook example * Add JSDocs for Storybook docs * Apply Overloaded naming convention * Update README * Fix typo * Update legacy storybook title/id, make sure JSDocs refer to unstable version * Derive types instead of importing them directly from ariakit * Add JSDoc snippet for stable component * Remove unnecessary JSDoc code * Remove unnecessary display name * Assign display names via Object.assign to comply with TS and get correct results in Storybook * Update subcomponent TS ignore comment to align with other components * Remove unnecessary store prop in circular option picker Composite.Item should pick up the store from context without explicit prop * Add first-party types, rewrite components with one unique forwardRef call * Use the newly added types instead of using the Parameters<> util * Fix Storybook story type * Remove unnecessary ts-expect-error * Use `CompositeStore` type directly * Manual Storybook args table * Tweak display name fallback * README * Mark `store` prop on `Composite` as required --- Co-authored-by: ciampo Co-authored-by: tyxla Co-authored-by: mirka <0mirka00@git.wordpress.org> --- docs/manifest.json | 6 + packages/components/CHANGELOG.md | 4 + .../src/alignment-matrix-control/cell.tsx | 6 +- .../src/alignment-matrix-control/index.tsx | 6 +- .../circular-option-picker-option.tsx | 11 +- .../circular-option-picker.tsx | 2 +- .../src/circular-option-picker/types.ts | 5 +- packages/components/src/composite/README.md | 176 ++++++++++++++ .../components/src/composite/current/index.ts | 20 -- .../composite/current/stories/index.story.tsx | 86 ------- packages/components/src/composite/index.ts | 7 - packages/components/src/composite/index.tsx | 177 ++++++++++++++ .../components/src/composite/legacy/index.tsx | 29 +-- .../composite/legacy/stories/index.story.tsx | 3 +- .../src/composite/legacy/stories/utils.tsx | 19 ++ .../src/composite/stories/index.story.tsx | 218 ++++++++++++++++++ .../composite/{current => }/stories/utils.tsx | 13 +- packages/components/src/composite/types.ts | 47 ++++ packages/components/src/composite/v2.ts | 4 - packages/components/src/index.ts | 3 +- packages/components/src/private-apis.ts | 18 +- .../src/dataviews-layouts/list/index.tsx | 1 + 22 files changed, 700 insertions(+), 161 deletions(-) create mode 100644 packages/components/src/composite/README.md delete mode 100644 packages/components/src/composite/current/index.ts delete mode 100644 packages/components/src/composite/current/stories/index.story.tsx delete mode 100644 packages/components/src/composite/index.ts create mode 100644 packages/components/src/composite/index.tsx create mode 100644 packages/components/src/composite/stories/index.story.tsx rename packages/components/src/composite/{current => }/stories/utils.tsx (86%) create mode 100644 packages/components/src/composite/types.ts delete mode 100644 packages/components/src/composite/v2.ts diff --git a/docs/manifest.json b/docs/manifest.json index 1704e6d711510f..b483449872cc76 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -767,6 +767,12 @@ "markdown_source": "../packages/components/src/combobox-control/README.md", "parent": "components" }, + { + "title": "Composite", + "slug": "composite", + "markdown_source": "../packages/components/src/composite/README.md", + "parent": "components" + }, { "title": "ConfirmDialog", "slug": "confirm-dialog", diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 47afb0711090d8..dca5b6e03dedc3 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### New Features + +- `Composite`: add stable version of the component ([#63564](https://github.com/WordPress/gutenberg/pull/63564)). + ### Enhancements - `TimePicker`: add `hideLabelFromVision` prop ([#64267](https://github.com/WordPress/gutenberg/pull/64267)). diff --git a/packages/components/src/alignment-matrix-control/cell.tsx b/packages/components/src/alignment-matrix-control/cell.tsx index 162ca879f1a7e5..6e045c26694f4e 100644 --- a/packages/components/src/alignment-matrix-control/cell.tsx +++ b/packages/components/src/alignment-matrix-control/cell.tsx @@ -1,7 +1,7 @@ /** * Internal dependencies */ -import { CompositeItem } from '../composite/v2'; +import { Composite } from '../composite'; import Tooltip from '../tooltip'; import { VisuallyHidden } from '../visually-hidden'; @@ -26,7 +26,7 @@ export default function Cell( { return ( - } > @@ -35,7 +35,7 @@ export default function Cell( { hidden element instead of aria-label. */ } { value } - + ); } diff --git a/packages/components/src/alignment-matrix-control/index.tsx b/packages/components/src/alignment-matrix-control/index.tsx index eaec8a285b0c57..1d22c3560625db 100644 --- a/packages/components/src/alignment-matrix-control/index.tsx +++ b/packages/components/src/alignment-matrix-control/index.tsx @@ -13,7 +13,7 @@ import { useInstanceId } from '@wordpress/compose'; * Internal dependencies */ import Cell from './cell'; -import { Composite, CompositeRow, useCompositeStore } from '../composite/v2'; +import { Composite, useCompositeStore } from '../composite'; import { Root, Row } from './styles/alignment-matrix-control-styles'; import AlignmentMatrixControlIcon from './icon'; import { GRID, getItemId, getItemValue } from './utils'; @@ -87,7 +87,7 @@ export function AlignmentMatrixControl( { } > { GRID.map( ( cells, index ) => ( - } key={ index }> + } key={ index }> { cells.map( ( cell ) => { const cellId = getItemId( baseId, cell ); const isActive = cellId === activeId; @@ -101,7 +101,7 @@ export function AlignmentMatrixControl( { /> ); } ) } - + ) ) } ); diff --git a/packages/components/src/circular-option-picker/circular-option-picker-option.tsx b/packages/components/src/circular-option-picker/circular-option-picker-option.tsx index 6c00a0e5d0bf1a..35a2f427134f40 100644 --- a/packages/components/src/circular-option-picker/circular-option-picker-option.tsx +++ b/packages/components/src/circular-option-picker/circular-option-picker-option.tsx @@ -16,9 +16,9 @@ import { Icon, check } from '@wordpress/icons'; */ import { CircularOptionPickerContext } from './circular-option-picker-context'; import Button from '../button'; -import { CompositeItem } from '../composite/v2'; +import { Composite } from '../composite'; import Tooltip from '../tooltip'; -import type { OptionProps, CircularOptionPickerCompositeStore } from './types'; +import type { OptionProps } from './types'; function UnforwardedOptionAsButton( props: { @@ -45,7 +45,9 @@ function UnforwardedOptionAsOption( id: string; className?: string; isSelected?: boolean; - compositeStore: CircularOptionPickerCompositeStore; + compositeStore: NonNullable< + React.ComponentProps< typeof Composite >[ 'store' ] + >; }, forwardedRef: ForwardedRef< any > ) { @@ -57,7 +59,7 @@ function UnforwardedOptionAsOption( } return ( - } - store={ compositeStore } id={ id } /> ); diff --git a/packages/components/src/circular-option-picker/circular-option-picker.tsx b/packages/components/src/circular-option-picker/circular-option-picker.tsx index cd2ddcf90d7ce0..c1e719f2d4f665 100644 --- a/packages/components/src/circular-option-picker/circular-option-picker.tsx +++ b/packages/components/src/circular-option-picker/circular-option-picker.tsx @@ -13,7 +13,7 @@ import { isRTL } from '@wordpress/i18n'; * Internal dependencies */ import { CircularOptionPickerContext } from './circular-option-picker-context'; -import { Composite, useCompositeStore } from '../composite/v2'; +import { Composite, useCompositeStore } from '../composite'; import type { CircularOptionPickerProps, ListboxCircularOptionPickerProps, diff --git a/packages/components/src/circular-option-picker/types.ts b/packages/components/src/circular-option-picker/types.ts index 519d81d5905107..e23ff4165f0580 100644 --- a/packages/components/src/circular-option-picker/types.ts +++ b/packages/components/src/circular-option-picker/types.ts @@ -14,7 +14,7 @@ import type { Icon } from '@wordpress/icons'; import type { ButtonAsButtonProps } from '../button/types'; import type { DropdownProps } from '../dropdown/types'; import type { WordPressComponentProps } from '../context'; -import type { CompositeStore } from '../composite/v2'; +import type { Composite } from '../composite'; type CommonCircularOptionPickerProps = { /** @@ -123,8 +123,7 @@ export type OptionProps = Omit< >; }; -export type CircularOptionPickerCompositeStore = CompositeStore; export type CircularOptionPickerContextProps = { baseId?: string; - compositeStore?: CircularOptionPickerCompositeStore; + compositeStore?: React.ComponentProps< typeof Composite >[ 'store' ]; }; diff --git a/packages/components/src/composite/README.md b/packages/components/src/composite/README.md new file mode 100644 index 00000000000000..59953f1273a054 --- /dev/null +++ b/packages/components/src/composite/README.md @@ -0,0 +1,176 @@ +# `Composite` + +`Composite` provides a single tab stop on the page and allows navigation through the focusable descendants with arrow keys. This abstract component is based on the [WAI-ARIA Composite Role⁠](https://w3c.github.io/aria/#composite). + +See the [Ariakit docs for the `Composite` component](https://ariakit.org/components/composite). + +## Usage + +```jsx +const store = useCompositeStore(); + + + Label + Item 1 + Item 2 + + +``` + +## Hooks + +### `useCompositeStore` + +Creates a composite store. + +#### Props + +##### `activeId`: `string | null` + +The current active item id. The active item is the element within the composite widget that has either DOM or virtual focus. + +- Required: no + +##### `defaultActiveId`: `string | null` + +The composite item id that should be active by default when the composite widget is rendered. If `null`, the composite element itself will have focus and users will be able to navigate to it using arrow keys. If `undefined`, the first enabled item will be focused. + +- Required: no + +##### `setActiveId`: `((activeId: string | null | undefined) => void)` + +A callback that gets called when the activeId state changes. + +- Required: no + +##### `focusLoop`: `boolean | 'horizontal' | 'vertical' | 'both'` + +Determines how the focus behaves when the user reaches the end of the composite widget. + +- Required: no +- Default: `false` + +##### `focusShift`: `boolean` + +Works only on two-dimensional composite widgets. If enabled, moving up or down when there's no next item or when the next item is disabled will shift to the item right before it. + +- Required: no +- Default: `false` + +##### `focusWrap`: `boolean` + +Works only on two-dimensional composite widgets. If enabled, moving to the next item from the last one in a row or column will focus on the first item in the next row or column and vice-versa. + +- Required: no +- Default: `false` + +##### `virtualFocus`: `boolean` + +If enabled, the composite element will act as an aria-activedescendant⁠ container instead of roving tabindex⁠. DOM focus will remain on the composite element while its items receive virtual focus. In both scenarios, the item in focus will carry the data-active-item attribute. + +- Required: no +- Default: `false` + +##### `orientation`: `'horizontal' | 'vertical' | 'both'` + +Defines the orientation of the composite widget. If the composite has a single row or column (one-dimensional), the orientation value determines which arrow keys can be used to move focus. It doesn't have any effect on two-dimensional composites. + +- Required: no +- Default: `'both'` + +##### `rtl`: `boolean` + +Determines how the next and previous functions will behave. If rtl is set to true, they will be inverted. This only affects the composite widget behavior. You still need to set dir=`rtl` on HTML/CSS. + +- Required: no +- Default: `false` + +## Components + +### `Composite` + +Renders a composite widget. + +#### Props + +##### `store`: `CompositeStore` + +Object returned by the `useCompositeStore` hook. + +- Required: yes + +##### `render`: `RenderProp & { ref?: React.Ref | undefined; }> | React.ReactElement>` + +Allows the component to be rendered as a different HTML element or React component. The value can be a React element or a function that takes in the original component props and gives back a React element with the props merged. + +- Required: no + +##### `children`: `React.ReactNode` + +The contents of the component. + +- Required: no + +### `Composite.Group` + +Renders a group element for composite items. + +##### `render`: `RenderProp & { ref?: React.Ref | undefined; }> | React.ReactElement>` + +Allows the component to be rendered as a different HTML element or React component. The value can be a React element or a function that takes in the original component props and gives back a React element with the props merged. + +- Required: no + +##### `children`: `React.ReactNode` + +The contents of the component. + +- Required: no + +### `Composite.GroupLabel` + +Renders a label in a composite group. This component must be wrapped with `Composite.Group` so the `aria-labelledby` prop is properly set on the composite group element. + +##### `render`: `RenderProp & { ref?: React.Ref | undefined; }> | React.ReactElement>` + +Allows the component to be rendered as a different HTML element or React component. The value can be a React element or a function that takes in the original component props and gives back a React element with the props merged. + +- Required: no + +##### `children`: `React.ReactNode` + +The contents of the component. + +- Required: no + +### `Composite.Item` + +Renders a composite item. + +##### `render`: `RenderProp & { ref?: React.Ref | undefined; }> | React.ReactElement>` + +Allows the component to be rendered as a different HTML element or React component. The value can be a React element or a function that takes in the original component props and gives back a React element with the props merged. + +- Required: no + +##### `children`: `React.ReactNode` + +The contents of the component. + +- Required: no + +### `Composite.Row` + +Renders a composite row. Wrapping `Composite.Item` elements within `Composite.Row` will create a two-dimensional composite widget, such as a grid. + +##### `render`: `RenderProp & { ref?: React.Ref | undefined; }> | React.ReactElement>` + +Allows the component to be rendered as a different HTML element or React component. The value can be a React element or a function that takes in the original component props and gives back a React element with the props merged. + +- Required: no + +##### `children`: `React.ReactNode` + +The contents of the component. + +- Required: no diff --git a/packages/components/src/composite/current/index.ts b/packages/components/src/composite/current/index.ts deleted file mode 100644 index 96379f00296516..00000000000000 --- a/packages/components/src/composite/current/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Composite is a component that may contain navigable items represented by - * CompositeItem. It's inspired by the WAI-ARIA Composite Role and implements - * all the keyboard navigation mechanisms to ensure that there's only one - * tab stop for the whole Composite element. This means that it can behave as - * a roving tabindex or aria-activedescendant container. - * - * @see https://ariakit.org/components/composite - */ - -export { - Composite, - CompositeGroup, - CompositeGroupLabel, - CompositeItem, - CompositeRow, - useCompositeStore, -} from '@ariakit/react'; - -export type { CompositeStore, CompositeStoreProps } from '@ariakit/react'; diff --git a/packages/components/src/composite/current/stories/index.story.tsx b/packages/components/src/composite/current/stories/index.story.tsx deleted file mode 100644 index 335ebc3244c918..00000000000000 --- a/packages/components/src/composite/current/stories/index.story.tsx +++ /dev/null @@ -1,86 +0,0 @@ -/** - * External dependencies - */ -import type { Meta, StoryFn } from '@storybook/react'; - -/** - * WordPress dependencies - */ -import { isRTL } from '@wordpress/i18n'; - -/** - * Internal dependencies - */ -import { - Composite, - CompositeGroup, - CompositeRow, - CompositeItem, - useCompositeStore, -} from '..'; -import { UseCompositeStorePlaceholder, transform } from './utils'; - -const meta: Meta< typeof UseCompositeStorePlaceholder > = { - title: 'Components/Composite (V2)', - component: UseCompositeStorePlaceholder, - subcomponents: { - // @ts-expect-error - See https://github.com/storybookjs/storybook/issues/23170 - Composite, - // @ts-expect-error - See https://github.com/storybookjs/storybook/issues/23170 - CompositeGroup, - // @ts-expect-error - See https://github.com/storybookjs/storybook/issues/23170 - CompositeRow, - // @ts-expect-error - See https://github.com/storybookjs/storybook/issues/23170 - CompositeItem, - }, - tags: [ 'status-private' ], - parameters: { - docs: { - canvas: { sourceState: 'shown' }, - source: { transform }, - extractArgTypes: ( component: React.FunctionComponent ) => { - const name = component.displayName; - const path = name - ?.replace( - /([a-z])([A-Z])/g, - ( _, a, b ) => `${ a }-${ b.toLowerCase() }` - ) - .toLowerCase(); - const url = `https://ariakit.org/reference/${ path }`; - return { - props: { - name: 'Props', - description: `See Ariakit docs for ${ name }`, - table: { type: { summary: undefined } }, - }, - }; - }, - }, - }, -}; -export default meta; - -export const Default: StoryFn< typeof Composite > = ( { ...initialState } ) => { - const rtl = isRTL(); - const store = useCompositeStore( { rtl, ...initialState } ); - - return ( - - - Item A1 - Item A2 - Item A3 - - - Item B1 - Item B2 - Item B3 - - - Item C1 - Item C2 - Item C3 - - - ); -}; diff --git a/packages/components/src/composite/index.ts b/packages/components/src/composite/index.ts deleted file mode 100644 index aa06a6adf36ef2..00000000000000 --- a/packages/components/src/composite/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -// Originally this pointed at a Reakit implementation of -// `Composite`, but we are removing Reakit entirely from the -// codebase. We will continue to support the Reakit API -// through the 'legacy' version, which uses Ariakit under -// the hood. - -export * from './legacy'; diff --git a/packages/components/src/composite/index.tsx b/packages/components/src/composite/index.tsx new file mode 100644 index 00000000000000..9496bdb9e98664 --- /dev/null +++ b/packages/components/src/composite/index.tsx @@ -0,0 +1,177 @@ +/** + * Composite is a component that may contain navigable items represented by + * Composite.Item. It's inspired by the WAI-ARIA Composite Role and implements + * all the keyboard navigation mechanisms to ensure that there's only one + * tab stop for the whole Composite element. This means that it can behave as + * a roving tabindex or aria-activedescendant container. + * + * @see https://ariakit.org/components/composite + */ + +/** + * External dependencies + */ +import * as Ariakit from '@ariakit/react'; + +/** + * WordPress dependencies + */ +import { forwardRef } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import type { WordPressComponentProps } from '../context'; +import type { + CompositeStoreProps, + CompositeProps, + CompositeGroupProps, + CompositeGroupLabelProps, + CompositeItemProps, + CompositeRowProps, +} from './types'; + +/** + * Creates a composite store. + * @param props + * @see https://ariakit.org/reference/use-composite-store + * @example + * ```jsx + * const store = useCompositeStore(); + * + * Item + * Item + * Item + * + * ``` + */ +export function useCompositeStore( props: CompositeStoreProps ) { + return Ariakit.useCompositeStore( props ); +} + +const Group = forwardRef< + HTMLDivElement, + WordPressComponentProps< CompositeGroupProps, 'div', false > +>( function CompositeGroup( props, ref ) { + return ; +} ); +Group.displayName = 'Composite.Group'; + +const GroupLabel = forwardRef< + HTMLDivElement, + WordPressComponentProps< CompositeGroupLabelProps, 'div', false > +>( function CompositeGroupLabel( props, ref ) { + return ; +} ); +GroupLabel.displayName = 'Composite.GroupLabel'; + +const Item = forwardRef< + HTMLButtonElement, + WordPressComponentProps< CompositeItemProps, 'button', false > +>( function CompositeItem( props, ref ) { + return ; +} ); +Item.displayName = 'Composite.Item'; + +const Row = forwardRef< + HTMLDivElement, + WordPressComponentProps< CompositeRowProps, 'div', false > +>( function CompositeRow( props, ref ) { + return ; +} ); +Row.displayName = 'Composite.Row'; + +/** + * Renders a composite widget. + * @see https://ariakit.org/reference/composite + * @example + * ```jsx + * const store = useCompositeStore(); + * + * Item 1 + * Item 2 + * + * ``` + */ +export const Composite = Object.assign( + forwardRef< + HTMLDivElement, + WordPressComponentProps< CompositeProps, 'div', false > + >( function CompositeRow( props, ref ) { + return ; + } ), + { + displayName: 'Composite', + /** + * Renders a group element for composite items. + * @see https://ariakit.org/reference/composite-group + * @example + * ```jsx + * const store = useCompositeStore(); + * + * + * Label + * Item 1 + * Item 2 + * + * + * ``` + */ + Group, + /** + * Renders a label in a composite group. This component must be wrapped with + * `Composite.Group` so the `aria-labelledby` prop is properly set on the + * composite group element. + * @see https://ariakit.org/reference/composite-group-label + * @example + * ```jsx + * const store = useCompositeStore(); + * + * + * Label + * Item 1 + * Item 2 + * + * + * ``` + */ + GroupLabel, + /** + * Renders a composite item. + * @see https://ariakit.org/reference/composite-item + * @example + * ```jsx + * const store = useCompositeStore(); + * + * Item 1 + * Item 2 + * Item 3 + * + * ``` + */ + Item, + /** + * Renders a composite row. Wrapping `Composite.Item` elements within + * `Composite.Row` will create a two-dimensional composite widget, such as a + * grid. + * @see https://ariakit.org/reference/composite-row + * @example + * ```jsx + * const store = useCompositeStore(); + * + * + * Item 1.1 + * Item 1.2 + * Item 1.3 + * + * + * Item 2.1 + * Item 2.2 + * Item 2.3 + * + * + * ``` + */ + Row, + } +); diff --git a/packages/components/src/composite/legacy/index.tsx b/packages/components/src/composite/legacy/index.tsx index 5c5c674b5086b8..dffdc1a2066d47 100644 --- a/packages/components/src/composite/legacy/index.tsx +++ b/packages/components/src/composite/legacy/index.tsx @@ -5,6 +5,11 @@ * tab stop for the whole Composite element. This means that it can behave as * a roving tabindex or aria-activedescendant container. * + * This file aims at providing components that are as close as possible to the + * original `reakit`-based implementation (which was removed from the codebase), + * although it is recommended that consumers of the package switch to the stable, + * un-prefixed, `ariakit`-based version of `Composite`. + * * @see https://ariakit.org/components/composite */ @@ -16,7 +21,7 @@ import { forwardRef } from '@wordpress/element'; /** * Internal dependencies */ -import * as Current from '../current'; +import { Composite as Current, useCompositeStore } from '..'; import { useInstanceId } from '@wordpress/compose'; type Orientation = 'horizontal' | 'vertical'; @@ -73,7 +78,7 @@ export interface LegacyStateOptions { type Component = React.FunctionComponent< any >; -type CompositeStore = ReturnType< typeof Current.useCompositeStore >; +type CompositeStore = ReturnType< typeof useCompositeStore >; type CompositeStoreState = { store: CompositeStore }; export type CompositeState = CompositeStoreState & Required< Pick< LegacyStateOptions, 'baseId' > >; @@ -93,9 +98,9 @@ type CompositeComponent< C extends Component > = ( ) => React.ReactElement; type CompositeComponentProps = CompositeState & ( - | ComponentProps< typeof Current.CompositeGroup > - | ComponentProps< typeof Current.CompositeItem > - | ComponentProps< typeof Current.CompositeRow > + | ComponentProps< typeof Current.Group > + | ComponentProps< typeof Current.Item > + | ComponentProps< typeof Current.Row > ); function mapLegacyStatePropsToComponentProps( @@ -145,19 +150,15 @@ function proxyComposite< C extends Component >( // provided role, and returning the appropriate component. const unproxiedCompositeGroup = forwardRef< any, - React.ComponentPropsWithoutRef< - typeof Current.CompositeGroup | typeof Current.CompositeRow - > + React.ComponentPropsWithoutRef< typeof Current.Group | typeof Current.Row > >( ( { role, ...props }, ref ) => { - const Component = - role === 'row' ? Current.CompositeRow : Current.CompositeGroup; + const Component = role === 'row' ? Current.Row : Current.Group; return ; } ); -unproxiedCompositeGroup.displayName = 'CompositeGroup'; -export const Composite = proxyComposite( Current.Composite, { baseId: 'id' } ); +export const Composite = proxyComposite( Current, { baseId: 'id' } ); export const CompositeGroup = proxyComposite( unproxiedCompositeGroup ); -export const CompositeItem = proxyComposite( Current.CompositeItem, { +export const CompositeItem = proxyComposite( Current.Item, { focusable: 'accessibleWhenDisabled', } ); @@ -178,7 +179,7 @@ export function useCompositeState( return { baseId: useInstanceId( Composite, 'composite', baseId ), - store: Current.useCompositeStore( { + store: useCompositeStore( { defaultActiveId, rtl, orientation, diff --git a/packages/components/src/composite/legacy/stories/index.story.tsx b/packages/components/src/composite/legacy/stories/index.story.tsx index e46d656a16810e..1b8e07e9bbf560 100644 --- a/packages/components/src/composite/legacy/stories/index.story.tsx +++ b/packages/components/src/composite/legacy/stories/index.story.tsx @@ -15,7 +15,8 @@ import { import { UseCompositeStatePlaceholder, transform } from './utils'; const meta: Meta< typeof UseCompositeStatePlaceholder > = { - title: 'Components/Composite', + title: 'Components (Deprecated)/Composite (Unstable)', + id: 'components-composite-unstable', component: UseCompositeStatePlaceholder, subcomponents: { Composite, diff --git a/packages/components/src/composite/legacy/stories/utils.tsx b/packages/components/src/composite/legacy/stories/utils.tsx index 06edd348634695..2fb51c845f9fbe 100644 --- a/packages/components/src/composite/legacy/stories/utils.tsx +++ b/packages/components/src/composite/legacy/stories/utils.tsx @@ -8,6 +8,25 @@ import type { StoryContext } from '@storybook/react'; */ import type { LegacyStateOptions } from '..'; +/** + * Renders a composite widget. + * + * This unstable component is deprecated. Use `Composite` instead. + * + * ```jsx + * import { + * __unstableUseCompositeState as useCompositeState, + * __unstableComposite as Composite, + * __unstableCompositeItem as CompositeItem, + * } from '@wordpress/components'; + * + * const state = useCompositeState(); + * + * Item 1 + * Item 2 + * ; + * ``` + */ export function UseCompositeStatePlaceholder( props: LegacyStateOptions ) { return (
diff --git a/packages/components/src/composite/stories/index.story.tsx b/packages/components/src/composite/stories/index.story.tsx new file mode 100644 index 00000000000000..b143c1f7db05f7 --- /dev/null +++ b/packages/components/src/composite/stories/index.story.tsx @@ -0,0 +1,218 @@ +/** + * External dependencies + */ +import type { Meta, StoryFn } from '@storybook/react'; + +/** + * WordPress dependencies + */ +import { isRTL } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import { Composite, useCompositeStore } from '..'; +import { UseCompositeStorePlaceholder, transform } from './utils'; + +const meta: Meta< typeof UseCompositeStorePlaceholder > = { + title: 'Components/Composite', + component: UseCompositeStorePlaceholder, + subcomponents: { + // @ts-expect-error - See https://github.com/storybookjs/storybook/issues/23170 + Composite, + // @ts-expect-error - See https://github.com/storybookjs/storybook/issues/23170 + 'Composite.Group': Composite.Group, + // @ts-expect-error - See https://github.com/storybookjs/storybook/issues/23170 + 'Composite.GroupLabel': Composite.GroupLabel, + // @ts-expect-error - See https://github.com/storybookjs/storybook/issues/23170 + 'Composite.Row': Composite.Row, + // @ts-expect-error - See https://github.com/storybookjs/storybook/issues/23170 + 'Composite.Item': Composite.Item, + }, + parameters: { + docs: { + canvas: { sourceState: 'shown' }, + source: { transform }, + extractArgTypes: ( component: React.FunctionComponent ) => { + const commonArgTypes = { + render: { + name: 'render', + description: + 'Allows the component to be rendered as a different HTML element or React component. The value can be a React element or a function that takes in the original component props and gives back a React element with the props merged.', + table: { + type: { + summary: + 'RenderProp & { ref?: React.Ref | undefined; }> | React.ReactElement>', + }, + }, + }, + children: { + name: 'children', + description: 'The contents of the component.', + table: { type: { summary: 'React.ReactNode' } }, + }, + }; + const argTypes = { + useCompositeStore: { + activeId: { + name: 'activeId', + description: + 'The current active item id. The active item is the element within the composite widget that has either DOM or virtual focus.', + table: { type: { summary: 'string | null' } }, + }, + defaultActiveId: { + name: 'defaultActiveId', + description: + 'The composite item id that should be active by default when the composite widget is rendered. If `null`, the composite element itself will have focus and users will be able to navigate to it using arrow keys. If `undefined`, the first enabled item will be focused.', + table: { type: { summary: 'string | null' } }, + }, + setActiveId: { + name: 'setActiveId', + description: + 'A callback that gets called when the activeId state changes.', + table: { + type: { + summary: + '((activeId: string | null | undefined) => void)', + }, + }, + }, + focusLoop: { + name: 'focusLoop', + description: + 'Determines how the focus behaves when the user reaches the end of the composite widget.', + table: { + defaultValue: { + summary: 'false', + }, + type: { + summary: + "boolean | 'horizontal' | 'vertical' | 'both'", + }, + }, + }, + focusShift: { + name: 'focusShift', + description: + "Works only on two-dimensional composite widgets. If enabled, moving up or down when there's no next item or when the next item is disabled will shift to the item right before it.", + table: { + defaultValue: { + summary: 'false', + }, + type: { + summary: 'boolean', + }, + }, + }, + focusWrap: { + name: 'focusWrap', + description: + 'Works only on two-dimensional composite widgets. If enabled, moving to the next item from the last one in a row or column will focus on the first item in the next row or column and vice-versa.', + table: { + defaultValue: { + summary: 'false', + }, + type: { + summary: 'boolean', + }, + }, + }, + virtualFocus: { + name: 'virtualFocus', + description: + 'If enabled, the composite element will act as an aria-activedescendant⁠ container instead of roving tabindex⁠. DOM focus will remain on the composite element while its items receive virtual focus. In both scenarios, the item in focus will carry the data-active-item attribute.', + table: { + defaultValue: { + summary: 'false', + }, + type: { + summary: 'boolean', + }, + }, + }, + orientation: { + name: 'orientation', + description: + "Defines the orientation of the composite widget. If the composite has a single row or column (one-dimensional), the orientation value determines which arrow keys can be used to move focus. It doesn't have any effect on two-dimensional composites.", + table: { + defaultValue: { + summary: "'both'", + }, + type: { + summary: + "'horizontal' | 'vertical' | 'both'", + }, + }, + }, + rtl: { + name: 'rtl', + description: + 'Determines how the next and previous functions will behave. If rtl is set to true, they will be inverted. This only affects the composite widget behavior. You still need to set dir="rtl" on HTML/CSS.', + table: { + defaultValue: { + summary: 'false', + }, + type: { + summary: 'boolean', + }, + }, + }, + }, + Composite: { + ...commonArgTypes, + store: { + name: 'store', + description: + 'Object returned by the `useCompositeStore` hook.', + table: { + type: { + summary: + 'CompositeStore', + }, + }, + type: { required: true }, + }, + }, + 'Composite.Group': commonArgTypes, + 'Composite.GroupLabel': commonArgTypes, + 'Composite.Row': commonArgTypes, + 'Composite.Item': commonArgTypes, + }; + + const name = component.displayName ?? ''; + + return name in argTypes + ? argTypes[ name as keyof typeof argTypes ] + : {}; + }, + }, + }, +}; +export default meta; + +export const Default: StoryFn< typeof UseCompositeStorePlaceholder > = ( + storeProps +) => { + const rtl = isRTL(); + const store = useCompositeStore( { rtl, ...storeProps } ); + + return ( + + + Item A1 + Item A2 + Item A3 + + + Item B1 + Item B2 + Item B3 + + + Item C1 + Item C2 + Item C3 + + + ); +}; diff --git a/packages/components/src/composite/current/stories/utils.tsx b/packages/components/src/composite/stories/utils.tsx similarity index 86% rename from packages/components/src/composite/current/stories/utils.tsx rename to packages/components/src/composite/stories/utils.tsx index 4b2d1bba4b312b..425fb9a905bcff 100644 --- a/packages/components/src/composite/current/stories/utils.tsx +++ b/packages/components/src/composite/stories/utils.tsx @@ -6,8 +6,19 @@ import type { StoryContext } from '@storybook/react'; /** * Internal dependencies */ -import type { CompositeStoreProps } from '..'; +import type { CompositeStoreProps } from '../types'; +/** + * Renders a composite widget. + * + * ```jsx + * const store = useCompositeStore(); + * + * Item 1 + * Item 2 + * + * ``` + */ export function UseCompositeStorePlaceholder( props: CompositeStoreProps ) { return (
diff --git a/packages/components/src/composite/types.ts b/packages/components/src/composite/types.ts new file mode 100644 index 00000000000000..438d1caaa94f8a --- /dev/null +++ b/packages/components/src/composite/types.ts @@ -0,0 +1,47 @@ +/** + * External dependencies + */ +import type * as Ariakit from '@ariakit/react'; + +export type CompositeStoreProps = Pick< + Ariakit.CompositeStoreProps, + | 'activeId' + | 'defaultActiveId' + | 'setActiveId' + | 'focusLoop' + | 'focusShift' + | 'focusWrap' + | 'virtualFocus' + | 'orientation' + | 'rtl' +>; + +export type CompositeProps = Pick< + Ariakit.CompositeProps, + 'render' | 'children' +> & { + /** + * Object returned by the `useCompositeStore` hook. + */ + store: Ariakit.CompositeStore; +}; + +export type CompositeGroupProps = Pick< + Ariakit.CompositeGroupProps, + 'render' | 'children' +>; + +export type CompositeGroupLabelProps = Pick< + Ariakit.CompositeGroupLabelProps, + 'render' | 'children' +>; + +export type CompositeItemProps = Pick< + Ariakit.CompositeItemProps, + 'render' | 'children' +>; + +export type CompositeRowProps = Pick< + Ariakit.CompositeRowProps, + 'render' | 'children' +>; diff --git a/packages/components/src/composite/v2.ts b/packages/components/src/composite/v2.ts deleted file mode 100644 index 38d3f628d368b6..00000000000000 --- a/packages/components/src/composite/v2.ts +++ /dev/null @@ -1,4 +0,0 @@ -// Although we have migrated away from Reakit, the 'current' -// Ariakit implementation is still considered a v2. - -export * from './current'; diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index f0ea4a4b7e86b8..4c724a461e6775 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -61,7 +61,8 @@ export { CompositeGroup as __unstableCompositeGroup, CompositeItem as __unstableCompositeItem, useCompositeState as __unstableUseCompositeState, -} from './composite'; +} from './composite/legacy'; +export { Composite } from './composite'; export { ConfirmDialog as __experimentalConfirmDialog } from './confirm-dialog'; export { default as CustomSelectControl } from './custom-select-control'; export { default as Dashicon } from './dashicon'; diff --git a/packages/components/src/private-apis.ts b/packages/components/src/private-apis.ts index 5ff39ba364a041..699911e5ba046b 100644 --- a/packages/components/src/private-apis.ts +++ b/packages/components/src/private-apis.ts @@ -1,13 +1,7 @@ /** * Internal dependencies */ -import { - Composite as CompositeV2, - CompositeGroup as CompositeGroupV2, - CompositeItem as CompositeItemV2, - CompositeRow as CompositeRowV2, - useCompositeStore as useCompositeStoreV2, -} from './composite/v2'; +import { Composite, useCompositeStore } from './composite'; import { positionToPlacement as __experimentalPopoverLegacyPositionToPlacement } from './popover/utils'; import { createPrivateSlotFill } from './slot-fill'; import { @@ -28,11 +22,11 @@ import { lock } from './lock-unlock'; export const privateApis = {}; lock( privateApis, { - CompositeV2, - CompositeGroupV2, - CompositeItemV2, - CompositeRowV2, - useCompositeStoreV2, + CompositeV2: Composite, + CompositeGroupV2: Composite.Group, + CompositeItemV2: Composite.Item, + CompositeRowV2: Composite.Row, + useCompositeStoreV2: useCompositeStore, __experimentalPopoverLegacyPositionToPlacement, createPrivateSlotFill, ComponentsContext, diff --git a/packages/dataviews/src/dataviews-layouts/list/index.tsx b/packages/dataviews/src/dataviews-layouts/list/index.tsx index 00939a12984856..70eaf3af1cd619 100644 --- a/packages/dataviews/src/dataviews-layouts/list/index.tsx +++ b/packages/dataviews/src/dataviews-layouts/list/index.tsx @@ -2,6 +2,7 @@ * External dependencies */ import clsx from 'clsx'; +// TODO: use the @wordpress/components one once public // Import CompositeStore type, which is not exported from @wordpress/components. // eslint-disable-next-line no-restricted-imports import type { CompositeStore } from '@ariakit/react'; From 34b5219842367bb111cc010cb1db9a3a593812dd Mon Sep 17 00:00:00 2001 From: Ben Dwyer Date: Fri, 9 Aug 2024 11:30:20 +0100 Subject: [PATCH 023/126] Zoom Out: Don't hide the insertion point when hovering patterns (#64392) Co-authored-by: scruffian Co-authored-by: MaggieCabrera --- .../block-editor/src/components/inserter/menu.js | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/packages/block-editor/src/components/inserter/menu.js b/packages/block-editor/src/components/inserter/menu.js index e0bc29d62e1b9a..52e4e3062e9024 100644 --- a/packages/block-editor/src/components/inserter/menu.js +++ b/packages/block-editor/src/components/inserter/menu.js @@ -109,6 +109,7 @@ function InserterMenu( const onInsertPattern = useCallback( ( blocks, patternName ) => { + onToggleInsertionPoint( false ); onInsertBlocks( blocks, { patternName } ); onSelect(); }, @@ -123,13 +124,6 @@ function InserterMenu( [ onToggleInsertionPoint, setHoveredItem ] ); - const onHoverPattern = useCallback( - ( item ) => { - onToggleInsertionPoint( !! item ); - }, - [ onToggleInsertionPoint ] - ); - const onClickPatternCategory = useCallback( ( patternCategory, filter ) => { setSelectedPatternCategory( patternCategory ); @@ -176,7 +170,6 @@ function InserterMenu( filterValue={ delayedFilterValue } onSelect={ onSelect } onHover={ onHover } - onHoverPattern={ onHoverPattern } rootClientId={ rootClientId } clientId={ clientId } isAppender={ isAppender } @@ -199,7 +192,6 @@ function InserterMenu( delayedFilterValue, onSelect, onHover, - onHoverPattern, shouldFocusBlock, clientId, rootClientId, @@ -249,7 +241,6 @@ function InserterMenu( Date: Fri, 9 Aug 2024 19:56:49 +0900 Subject: [PATCH 024/126] DimensionControl: Add flag to remove bottom margin (#64346) * DimensionControl: Add flag to remove bottom margin * Add lint rule * Update in block-editor readme * Add changelog Co-authored-by: mirka <0mirka00@git.wordpress.org> Co-authored-by: tyxla --- .eslintrc.js | 1 + .../components/responsive-block-control/README.md | 1 + packages/components/CHANGELOG.md | 1 + packages/components/src/dimension-control/README.md | 9 +++++++++ packages/components/src/dimension-control/index.tsx | 3 +++ packages/components/src/dimension-control/types.ts | 12 +++++------- 6 files changed, 20 insertions(+), 7 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 81408499bd34f4..cb669fb4177206 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -292,6 +292,7 @@ module.exports = { ...[ 'CheckboxControl', 'ComboboxControl', + 'DimensionControl', 'FocalPointPicker', 'RangeControl', 'SearchControl', diff --git a/packages/block-editor/src/components/responsive-block-control/README.md b/packages/block-editor/src/components/responsive-block-control/README.md index 1d99caa024a419..379e245f03ff37 100644 --- a/packages/block-editor/src/components/responsive-block-control/README.md +++ b/packages/block-editor/src/components/responsive-block-control/README.md @@ -64,6 +64,7 @@ registerBlockType( 'my-plugin/my-block', { const paddingControl = ( labelComponent, viewport ) => { return ( setPaddingSize( value ) } @@ -91,3 +92,11 @@ A callback which is triggered when a spacing size value changes (is selected/cli - **Required:** No A string of classes to be added to the control component. + +### __nextHasNoMarginBottom + +Start opting into the new margin-free styles that will become the default in a future version. + +- Type: `Boolean` +- Required: No +- Default: `false` diff --git a/packages/components/src/dimension-control/index.tsx b/packages/components/src/dimension-control/index.tsx index 8ba0de0ce0599c..114ebe987dd35d 100644 --- a/packages/components/src/dimension-control/index.tsx +++ b/packages/components/src/dimension-control/index.tsx @@ -31,6 +31,7 @@ import type { SelectControlSingleSelectionProps } from '../select-control/types' * * return ( * setPaddingSize( value ) } @@ -43,6 +44,7 @@ import type { SelectControlSingleSelectionProps } from '../select-control/types' export function DimensionControl( props: DimensionControlProps ) { const { __next40pxDefaultSize = false, + __nextHasNoMarginBottom = false, label, value, sizes = sizesTable, @@ -87,6 +89,7 @@ export function DimensionControl( props: DimensionControlProps ) { return ( & { /** * Label for the control. */ @@ -45,10 +49,4 @@ export type DimensionControlProps = { * @default '' */ className?: string; - /** - * Start opting into the larger default height that will become the default size in a future version. - * - * @default false - */ - __next40pxDefaultSize?: boolean; }; From 00a965cc6d299b049c9ce9f317295cf9302deccf Mon Sep 17 00:00:00 2001 From: George Mamadashvili Date: Fri, 9 Aug 2024 15:13:15 +0400 Subject: [PATCH 025/126] Gallery: Remove 'withNotices' HoC (#64384) * Gallery: Remove 'withNotices' HoC * Relocate 'withViewportMatch' closer to its only usage Co-authored-by: Mamaduka Co-authored-by: geriux Co-authored-by: youknowriad --- packages/block-library/src/gallery/edit.js | 9 +-------- packages/block-library/src/gallery/gallery.native.js | 3 ++- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/packages/block-library/src/gallery/edit.js b/packages/block-library/src/gallery/edit.js index f2faa40110dd13..1a7ce5fae04f8c 100644 --- a/packages/block-library/src/gallery/edit.js +++ b/packages/block-library/src/gallery/edit.js @@ -6,7 +6,6 @@ import clsx from 'clsx'; /** * WordPress dependencies */ -import { compose } from '@wordpress/compose'; import { BaseControl, PanelBody, @@ -17,7 +16,6 @@ import { MenuGroup, MenuItem, ToolbarDropdownMenu, - withNotices, } from '@wordpress/components'; import { store as blockEditorStore, @@ -31,7 +29,6 @@ import { import { Platform, useEffect, useMemo } from '@wordpress/element'; import { __, _x, sprintf } from '@wordpress/i18n'; import { useSelect, useDispatch } from '@wordpress/data'; -import { withViewportMatch } from '@wordpress/viewport'; import { View } from '@wordpress/primitives'; import { createBlock } from '@wordpress/blocks'; import { createBlobURL } from '@wordpress/blob'; @@ -98,7 +95,7 @@ const MOBILE_CONTROL_PROPS_RANGE_CONTROL = Platform.isNative const DEFAULT_BLOCK = { name: 'core/image' }; const EMPTY_ARRAY = []; -function GalleryEdit( props ) { +export default function GalleryEdit( props ) { const { setAttributes, attributes, @@ -696,7 +693,3 @@ function GalleryEdit( props ) { ); } -export default compose( [ - withNotices, - withViewportMatch( { isNarrow: '< small' } ), -] )( GalleryEdit ); diff --git a/packages/block-library/src/gallery/gallery.native.js b/packages/block-library/src/gallery/gallery.native.js index 7fc280809db843..a5fa2db4fcf029 100644 --- a/packages/block-library/src/gallery/gallery.native.js +++ b/packages/block-library/src/gallery/gallery.native.js @@ -22,6 +22,7 @@ import { useState, useEffect } from '@wordpress/element'; import { mediaUploadSync } from '@wordpress/react-native-bridge'; import { WIDE_ALIGNMENTS } from '@wordpress/components'; import { useResizeObserver } from '@wordpress/compose'; +import { withViewportMatch } from '@wordpress/viewport'; const TILE_SPACING = 8; @@ -120,4 +121,4 @@ export const Gallery = ( props ) => { ); }; -export default Gallery; +export default withViewportMatch( { isNarrow: '< small' } )( Gallery ); From d31b30ed135d06963a7036206a5f4855880f63f2 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Fri, 9 Aug 2024 14:31:13 +0200 Subject: [PATCH 026/126] DataViews Quick Edit: Add the PostActions dropdown menu (#64393) Co-authored-by: youknowriad Co-authored-by: oandregal Co-authored-by: ntsekouras --- docs/reference-guides/slotfills/README.md | 9 ++--- .../src/components/post-actions/index.js | 36 +++++++++---------- .../src/components/post-card-panel/index.js | 13 +++++-- .../src/components/sidebar/post-summary.js | 7 +--- 4 files changed, 30 insertions(+), 35 deletions(-) diff --git a/docs/reference-guides/slotfills/README.md b/docs/reference-guides/slotfills/README.md index 8b56ed4ce98b41..5ae68cdb5cb071 100644 --- a/docs/reference-guides/slotfills/README.md +++ b/docs/reference-guides/slotfills/README.md @@ -70,11 +70,10 @@ export default function PostSummary( { onActionPerformed } ) { const { isRemovedPostStatusPanel } = useSelect( ( select ) => { // We use isEditorPanelRemoved to hide the panel if it was programmatically removed. We do // not use isEditorPanelEnabled since this panel should not be disabled through the UI. - const { isEditorPanelRemoved, getCurrentPostType } = + const { isEditorPanelRemoved } = select( editorStore ); return { isRemovedPostStatusPanel: isEditorPanelRemoved( PANEL_NAME ), - postType: getCurrentPostType(), }; }, [] ); @@ -85,11 +84,7 @@ export default function PostSummary( { onActionPerformed } ) { <> - } + onActionPerformed={ onActionPerformed } /> diff --git a/packages/editor/src/components/post-actions/index.js b/packages/editor/src/components/post-actions/index.js index 5b023956178938..9cc594233c8814 100644 --- a/packages/editor/src/components/post-actions/index.js +++ b/packages/editor/src/components/post-actions/index.js @@ -17,7 +17,6 @@ import { store as coreStore } from '@wordpress/core-data'; */ import { unlock } from '../../lock-unlock'; import { usePostActions } from './actions'; -import { store as editorStore } from '../../store'; const { DropdownMenuV2: DropdownMenu, @@ -27,25 +26,23 @@ const { kebabCase, } = unlock( componentsPrivateApis ); -export default function PostActions( { onActionPerformed, buttonProps } ) { +export default function PostActions( { postType, postId, onActionPerformed } ) { const [ isActionsMenuOpen, setIsActionsMenuOpen ] = useState( false ); - const { item, permissions, postType } = useSelect( ( select ) => { - const { getCurrentPostType, getCurrentPostId } = select( editorStore ); - const { getEditedEntityRecord, getEntityRecordPermissions } = unlock( - select( coreStore ) - ); - const _postType = getCurrentPostType(); - const _id = getCurrentPostId(); - return { - item: getEditedEntityRecord( 'postType', _postType, _id ), - permissions: getEntityRecordPermissions( - 'postType', - _postType, - _id - ), - postType: _postType, - }; - }, [] ); + const { item, permissions } = useSelect( + ( select ) => { + const { getEditedEntityRecord, getEntityRecordPermissions } = + unlock( select( coreStore ) ); + return { + item: getEditedEntityRecord( 'postType', postType, postId ), + permissions: getEntityRecordPermissions( + 'postType', + postType, + postId + ), + }; + }, + [ postId, postType ] + ); const itemWithPermissions = useMemo( () => { return { ...item, @@ -76,7 +73,6 @@ export default function PostActions( { onActionPerformed, buttonProps } ) { onClick={ () => setIsActionsMenuOpen( ! isActionsMenuOpen ) } - { ...buttonProps } /> } onOpenChange={ setIsActionsMenuOpen } diff --git a/packages/editor/src/components/post-card-panel/index.js b/packages/editor/src/components/post-card-panel/index.js index 5ac88b3dbc7f94..ed13af9b55a4aa 100644 --- a/packages/editor/src/components/post-card-panel/index.js +++ b/packages/editor/src/components/post-card-panel/index.js @@ -26,8 +26,13 @@ import { GLOBAL_POST_TYPES, } from '../../store/constants'; import { unlock } from '../../lock-unlock'; +import PostActions from '../post-actions'; -export default function PostCardPanel( { postType, postId, actions } ) { +export default function PostCardPanel( { + postType, + postId, + onActionPerformed, +} ) { const { isFrontPage, isPostsPage, title, icon, isSync } = useSelect( ( select ) => { const { __experimentalGetTemplateInfo } = select( editorStore ); @@ -105,7 +110,11 @@ export default function PostCardPanel( { postType, postId, actions } ) { ) } - { actions } +
); diff --git a/packages/editor/src/components/sidebar/post-summary.js b/packages/editor/src/components/sidebar/post-summary.js index 573a59de827188..b19848f2247063 100644 --- a/packages/editor/src/components/sidebar/post-summary.js +++ b/packages/editor/src/components/sidebar/post-summary.js @@ -8,7 +8,6 @@ import { useSelect } from '@wordpress/data'; * Internal dependencies */ import PluginPostStatusInfo from '../plugin-post-status-info'; -import PostActions from '../post-actions'; import PostAuthorPanel from '../post-author/panel'; import PostCardPanel from '../post-card-panel'; import PostContentInformation from '../post-content-information'; @@ -63,11 +62,7 @@ export default function PostSummary( { onActionPerformed } ) { - } + onActionPerformed={ onActionPerformed } /> From e44bc77a252e81a549652e6ab480d62cb8184d5e Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Fri, 9 Aug 2024 14:34:50 +0200 Subject: [PATCH 027/126] DataViews Quick Edit: Rely on the global save flow instead of a custom save button (#64389) Co-authored-by: youknowriad Co-authored-by: oandregal --- packages/dataviews/CHANGELOG.md | 1 + .../dataform/stories/index.story.tsx | 7 +- .../dataviews/src/field-types/datetime.tsx | 6 +- .../dataviews/src/field-types/integer.tsx | 5 +- packages/dataviews/src/field-types/text.tsx | 5 +- packages/dataviews/src/types.ts | 11 +-- .../src/components/post-edit/index.js | 73 ++++++------------- .../src/components/post-actions/actions.js | 7 +- .../src/dataviews/actions/reorder-page.tsx | 7 +- 9 files changed, 50 insertions(+), 72 deletions(-) diff --git a/packages/dataviews/CHANGELOG.md b/packages/dataviews/CHANGELOG.md index 239a69651a1f27..b1cb4504e72fad 100644 --- a/packages/dataviews/CHANGELOG.md +++ b/packages/dataviews/CHANGELOG.md @@ -16,6 +16,7 @@ - `setSelection` prop has been removed. Please use `onChangeSelection` instead. - `header` field property has been renamed to `label`. - `DataForm`'s `visibleFields` prop has been renamed to `fields`. +- `DataForm`'s `onChange` prop has been update to receive as argument only the fields that have changed. ### New features diff --git a/packages/dataviews/src/components/dataform/stories/index.story.tsx b/packages/dataviews/src/components/dataform/stories/index.story.tsx index 7f3c5ff879b72a..7147b9c2342638 100644 --- a/packages/dataviews/src/components/dataform/stories/index.story.tsx +++ b/packages/dataviews/src/components/dataform/stories/index.story.tsx @@ -109,7 +109,12 @@ export const Default = ( { type }: { type: 'panel' | 'regular' } ) => { ...form, type, } } - onChange={ setPost } + onChange={ ( edits ) => + setPost( ( prev ) => ( { + ...prev, + ...edits, + } ) ) + } /> ); }; diff --git a/packages/dataviews/src/field-types/datetime.tsx b/packages/dataviews/src/field-types/datetime.tsx index 3861a0db635d7b..c6b69048efe13c 100644 --- a/packages/dataviews/src/field-types/datetime.tsx +++ b/packages/dataviews/src/field-types/datetime.tsx @@ -41,11 +41,7 @@ function Edit< Item >( { const value = field.getValue( { item: data } ); const onChangeControl = useCallback( - ( newValue: string | null ) => - onChange( ( prevItem: Item ) => ( { - ...prevItem, - [ id ]: newValue, - } ) ), + ( newValue: string | null ) => onChange( { [ id ]: newValue } ), [ id, onChange ] ); diff --git a/packages/dataviews/src/field-types/integer.tsx b/packages/dataviews/src/field-types/integer.tsx index bd4956493232df..38570ea6fec1a5 100644 --- a/packages/dataviews/src/field-types/integer.tsx +++ b/packages/dataviews/src/field-types/integer.tsx @@ -51,10 +51,9 @@ function Edit< Item >( { const value = field.getValue( { item: data } ) ?? ''; const onChangeControl = useCallback( ( newValue: string | undefined ) => - onChange( ( prevItem: Item ) => ( { - ...prevItem, + onChange( { [ id ]: Number( newValue ), - } ) ), + } ), [ id, onChange ] ); diff --git a/packages/dataviews/src/field-types/text.tsx b/packages/dataviews/src/field-types/text.tsx index c6efb85f6f446b..5364017c629b00 100644 --- a/packages/dataviews/src/field-types/text.tsx +++ b/packages/dataviews/src/field-types/text.tsx @@ -42,10 +42,9 @@ function Edit< Item >( { const onChangeControl = useCallback( ( newValue: string ) => - onChange( ( prevItem: Item ) => ( { - ...prevItem, + onChange( { [ id ]: newValue, - } ) ), + } ), [ id, onChange ] ); diff --git a/packages/dataviews/src/types.ts b/packages/dataviews/src/types.ts index d8a5ee8f68ecef..34e74eabd7c7d8 100644 --- a/packages/dataviews/src/types.ts +++ b/packages/dataviews/src/types.ts @@ -1,12 +1,7 @@ /** * External dependencies */ -import type { - ReactElement, - ComponentType, - Dispatch, - SetStateAction, -} from 'react'; +import type { ReactElement, ComponentType } from 'react'; /** * Internal dependencies @@ -181,7 +176,7 @@ export type Form = { export type DataFormControlProps< Item > = { data: Item; field: NormalizedField< Item >; - onChange: Dispatch< SetStateAction< Item > >; + onChange: ( value: Record< string, any > ) => void; hideLabelFromVision?: boolean; }; @@ -516,5 +511,5 @@ export interface DataFormProps< Item > { data: Item; fields: Field< Item >[]; form: Form; - onChange: Dispatch< SetStateAction< Item > >; + onChange: ( value: Record< string, any > ) => void; } diff --git a/packages/edit-site/src/components/post-edit/index.js b/packages/edit-site/src/components/post-edit/index.js index 0a56fdfe5786b7..80304f16503705 100644 --- a/packages/edit-site/src/components/post-edit/index.js +++ b/packages/edit-site/src/components/post-edit/index.js @@ -7,15 +7,11 @@ import clsx from 'clsx'; * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { DataForm, isItemValid } from '@wordpress/dataviews'; -import { useDispatch, useSelect, useRegistry } from '@wordpress/data'; +import { DataForm } from '@wordpress/dataviews'; +import { useDispatch, useSelect } from '@wordpress/data'; import { store as coreDataStore } from '@wordpress/core-data'; -import { - Button, - FlexItem, - __experimentalVStack as VStack, -} from '@wordpress/components'; -import { useState, useMemo } from '@wordpress/element'; +import { __experimentalVStack as VStack } from '@wordpress/components'; +import { useState, useMemo, useEffect } from '@wordpress/element'; import { privateApis as editorPrivateApis } from '@wordpress/editor'; /** @@ -29,14 +25,12 @@ const { PostCardPanel } = unlock( editorPrivateApis ); function PostEditForm( { postType, postId } ) { const ids = useMemo( () => postId.split( ',' ), [ postId ] ); - const { initialEdits } = useSelect( + const { record } = useSelect( ( select ) => { - if ( ids.length !== 1 ) { - } return { - initialEdits: + record: ids.length === 1 - ? select( coreDataStore ).getEntityRecord( + ? select( coreDataStore ).getEditedEntityRecord( 'postType', postType, ids[ 0 ] @@ -46,60 +40,39 @@ function PostEditForm( { postType, postId } ) { }, [ postType, ids ] ); - const registry = useRegistry(); - const { saveEntityRecord } = useDispatch( coreDataStore ); + const [ multiEdits, setMultiEdits ] = useState( {} ); + const { editEntityRecord } = useDispatch( coreDataStore ); const { fields } = usePostFields(); const form = { type: 'panel', fields: [ 'title', 'author', 'date', 'comment_status' ], }; - const [ edits, setEdits ] = useState( initialEdits ); - const itemWithEdits = useMemo( () => { - return { - ...initialEdits, - ...edits, - }; - }, [ initialEdits, edits ] ); - const onSubmit = async ( event ) => { - event.preventDefault(); - - if ( ! isItemValid( itemWithEdits, fields, form ) ) { - return; - } - - const { getEntityRecord } = registry.resolveSelect( coreDataStore ); + const onChange = ( edits ) => { for ( const id of ids ) { - const item = await getEntityRecord( 'postType', postType, id ); - saveEntityRecord( 'postType', postType, { - ...item, - ...edits, - } ); + editEntityRecord( 'postType', postType, id, edits ); + if ( ids.length > 1 ) { + setMultiEdits( ( prev ) => ( { + ...prev, + ...edits, + } ) ); + } } }; + useEffect( () => { + setMultiEdits( {} ); + }, [ ids ] ); - const isUpdateDisabled = ! isItemValid( itemWithEdits, fields, form ); return ( - + { ids.length === 1 && ( ) } - - - ); } diff --git a/packages/editor/src/components/post-actions/actions.js b/packages/editor/src/components/post-actions/actions.js index 5e727376a92258..ff5cd7ddbb5452 100644 --- a/packages/editor/src/components/post-actions/actions.js +++ b/packages/editor/src/components/post-actions/actions.js @@ -234,7 +234,12 @@ const useDuplicatePostAction = ( postType ) => { data={ item } fields={ fields } form={ formDuplicateAction } - onChange={ setItem } + onChange={ ( changes ) => + setItem( { + ...item, + ...changes, + } ) + } /> ) } - + diff --git a/packages/components/src/base-control/README.md b/packages/components/src/base-control/README.md index dc3d8c0e29c8e0..d51629de6f7253 100644 --- a/packages/components/src/base-control/README.md +++ b/packages/components/src/base-control/README.md @@ -15,7 +15,7 @@ const MyCustomTextareaControl = ({ children, ...baseProps }) => ( const { baseControlProps, controlProps } = useBaseControlProps( baseProps ); return ( - + @@ -92,7 +92,10 @@ It should only be used in cases where the children being rendered inside BaseCon import { BaseControl } from '@wordpress/components'; const MyBaseControl = () => ( - + Author diff --git a/packages/components/src/base-control/index.tsx b/packages/components/src/base-control/index.tsx index 14ecce1bdd729d..77899b6480daed 100644 --- a/packages/components/src/base-control/index.tsx +++ b/packages/components/src/base-control/index.tsx @@ -26,29 +26,6 @@ import { contextConnectWithoutRef, useContextSystem } from '../context'; export { useBaseControlProps } from './hooks'; -/** - * `BaseControl` is a component used to generate labels and help text for components handling user inputs. - * - * ```jsx - * import { BaseControl, useBaseControlProps } from '@wordpress/components'; - * - * // Render a `BaseControl` for a textarea input - * const MyCustomTextareaControl = ({ children, ...baseProps }) => ( - * // `useBaseControlProps` is a convenience hook to get the props for the `BaseControl` - * // and the inner control itself. Namely, it takes care of generating a unique `id`, - * // properly associating it with the `label` and `help` elements. - * const { baseControlProps, controlProps } = useBaseControlProps( baseProps ); - * - * return ( - * - * - * - * ); - * ); - * ``` - */ const UnconnectedBaseControl = ( props: WordPressComponentProps< BaseControlProps, null > ) => { @@ -105,23 +82,6 @@ const UnconnectedBaseControl = ( ); }; -/** - * `BaseControl.VisualLabel` is used to render a purely visual label inside a `BaseControl` component. - * - * It should only be used in cases where the children being rendered inside `BaseControl` are already accessibly labeled, - * e.g., a button, but we want an additional visual label for that section equivalent to the labels `BaseControl` would - * otherwise use if the `label` prop was passed. - * - * @example - * import { BaseControl } from '@wordpress/components'; - * - * const MyBaseControl = () => ( - * - * Author - * - * - * ); - */ const UnforwardedVisualLabel = ( props: WordPressComponentProps< BaseControlVisualLabelProps, 'span' >, ref: ForwardedRef< any > @@ -141,9 +101,56 @@ const UnforwardedVisualLabel = ( export const VisualLabel = forwardRef( UnforwardedVisualLabel ); +/** + * `BaseControl` is a component used to generate labels and help text for components handling user inputs. + * + * ```jsx + * import { BaseControl, useBaseControlProps } from '@wordpress/components'; + * + * // Render a `BaseControl` for a textarea input + * const MyCustomTextareaControl = ({ children, ...baseProps }) => ( + * // `useBaseControlProps` is a convenience hook to get the props for the `BaseControl` + * // and the inner control itself. Namely, it takes care of generating a unique `id`, + * // properly associating it with the `label` and `help` elements. + * const { baseControlProps, controlProps } = useBaseControlProps( baseProps ); + * + * return ( + * + * + * + * ); + * ); + * ``` + */ export const BaseControl = Object.assign( contextConnectWithoutRef( UnconnectedBaseControl, 'BaseControl' ), - { VisualLabel } + + { + /** + * `BaseControl.VisualLabel` is used to render a purely visual label inside a `BaseControl` component. + * + * It should only be used in cases where the children being rendered inside `BaseControl` are already accessibly labeled, + * e.g., a button, but we want an additional visual label for that section equivalent to the labels `BaseControl` would + * otherwise use if the `label` prop was passed. + * + * ```jsx + * import { BaseControl } from '@wordpress/components'; + * + * const MyBaseControl = () => ( + * + * Author + * + * + * ); + * ``` + */ + VisualLabel, + } ); export default BaseControl; diff --git a/packages/edit-site/src/components/global-styles/size-control/index.js b/packages/edit-site/src/components/global-styles/size-control/index.js index a7e7bd6127a5fb..28fa64c643cbbd 100644 --- a/packages/edit-site/src/components/global-styles/size-control/index.js +++ b/packages/edit-site/src/components/global-styles/size-control/index.js @@ -20,7 +20,11 @@ import { const DEFAULT_UNITS = [ 'px', 'em', 'rem', 'vw', 'vh' ]; -function SizeControl( props ) { +function SizeControl( { + // Do not allow manipulation of margin bottom + __nextHasNoMarginBottom, + ...props +} ) { const { baseControlProps } = useBaseControlProps( props ); const { value, onChange, fallbackValue, disabled } = props; @@ -45,7 +49,7 @@ function SizeControl( props ) { }; return ( - + Date: Mon, 12 Aug 2024 19:34:49 +0200 Subject: [PATCH 049/126] fix typo in block-filters.md (#64452) https://github.com/WordPress/gutenberg/pull/64447#pullrequestreview-2233534636 --- docs/reference-guides/filters/block-filters.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/reference-guides/filters/block-filters.md b/docs/reference-guides/filters/block-filters.md index c70bb356a445a8..637cecadf1402b 100644 --- a/docs/reference-guides/filters/block-filters.md +++ b/docs/reference-guides/filters/block-filters.md @@ -139,7 +139,7 @@ The following PHP filters are available to change the output of a block on the f ### `render_block` -Filters the font-end content of any block. This filter has no impact on the behavior of blocks in the Editor. +Filters the front-end content of any block. This filter has no impact on the behavior of blocks in the Editor. The callback function for this filter receives three parameters: @@ -172,7 +172,7 @@ add_filter( 'render_block', 'example_add_custom_class_to_paragraph_block', 10, 2 ### `render_block_{namespace/block}` -Filters the font-end content of the defined block. This is just a simpler form of `render_block` when you only need to modify a specific block type. +Filters the front-end content of the defined block. This is just a simpler form of `render_block` when you only need to modify a specific block type. The callback function for this filter receives three parameters: From 47138724bf9e6abc67f448e3403428d431f2a1b9 Mon Sep 17 00:00:00 2001 From: Aki Hamano <54422211+t-hamano@users.noreply.github.com> Date: Tue, 13 Aug 2024 13:33:25 +0900 Subject: [PATCH 050/126] In-between Inserter: Show inserter when it doesn't conflict with block toolbar (#64229) Unlinked contributors: digitalex11. Co-authored-by: t-hamano Co-authored-by: Mamaduka Co-authored-by: ellatrix Co-authored-by: annezazu Co-authored-by: talldan Co-authored-by: hanneslsm --- .../block-list/use-in-between-inserter.js | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/packages/block-editor/src/components/block-list/use-in-between-inserter.js b/packages/block-editor/src/components/block-list/use-in-between-inserter.js index 74151fb3b070ba..bb307816fd1501 100644 --- a/packages/block-editor/src/components/block-list/use-in-between-inserter.js +++ b/packages/block-editor/src/components/block-list/use-in-between-inserter.js @@ -25,6 +25,7 @@ export function useInBetweenInserter() { getBlockIndex, isMultiSelecting, getSelectedBlockClientIds, + getSettings, getTemplateLock, __unstableIsWithinBlockOverlay, getBlockEditingMode, @@ -88,9 +89,11 @@ export function useInBetweenInserter() { return; } + const blockListSettings = getBlockListSettings( rootClientId ); const orientation = - getBlockListSettings( rootClientId )?.orientation || - 'vertical'; + blockListSettings?.orientation || 'vertical'; + const captureToolbars = + !! blockListSettings?.__experimentalCaptureToolbars; const offsetTop = event.clientY; const offsetLeft = event.clientX; @@ -135,9 +138,18 @@ export function useInBetweenInserter() { return; } - // Don't show the inserter when hovering above (conflicts with - // block toolbar) or inside selected block(s). - if ( getSelectedBlockClientIds().includes( clientId ) ) { + // Don't show the inserter if the following conditions are met, + // as it conflicts with the block toolbar: + // 1. when hovering above or inside selected block(s) + // 2. when the orientation is vertical + // 3. when the __experimentalCaptureToolbars is not enabled + // 4. when the Top Toolbar is not disabled + if ( + getSelectedBlockClientIds().includes( clientId ) && + orientation === 'vertical' && + ! captureToolbars && + ! getSettings().hasFixedToolbar + ) { return; } const elementRect = element.getBoundingClientRect(); From 344d61e5df74411283014bcbb5dbbed0b54f000a Mon Sep 17 00:00:00 2001 From: George Mamadashvili Date: Tue, 13 Aug 2024 08:48:19 +0400 Subject: [PATCH 051/126] Block Editor: Use hooks instead of HoC in 'BlockModeToggle' (#64460) Co-authored-by: Mamaduka Co-authored-by: t-hamano --- .../block-settings-menu/block-mode-toggle.js | 59 +++++++++---------- .../test/block-mode-toggle.js | 49 +++++++-------- 2 files changed, 54 insertions(+), 54 deletions(-) diff --git a/packages/block-editor/src/components/block-settings-menu/block-mode-toggle.js b/packages/block-editor/src/components/block-settings-menu/block-mode-toggle.js index 6810a21581f12e..7ca294a2894158 100644 --- a/packages/block-editor/src/components/block-settings-menu/block-mode-toggle.js +++ b/packages/block-editor/src/components/block-settings-menu/block-mode-toggle.js @@ -4,8 +4,7 @@ import { __ } from '@wordpress/i18n'; import { MenuItem } from '@wordpress/components'; import { getBlockType, hasBlockSupport } from '@wordpress/blocks'; -import { withSelect, withDispatch } from '@wordpress/data'; -import { compose } from '@wordpress/compose'; +import { useDispatch, useSelect } from '@wordpress/data'; /** * Internal dependencies @@ -14,13 +13,23 @@ import { store as blockEditorStore } from '../../store'; const noop = () => {}; -export function BlockModeToggle( { - blockType, - mode, - onToggleMode, - small = false, - isCodeEditingEnabled = true, -} ) { +export default function BlockModeToggle( { clientId, onToggle = noop } ) { + const { blockType, mode, isCodeEditingEnabled } = useSelect( + ( select ) => { + const { getBlock, getBlockMode, getSettings } = + select( blockEditorStore ); + const block = getBlock( clientId ); + + return { + mode: getBlockMode( clientId ), + blockType: block ? getBlockType( block.name ) : null, + isCodeEditingEnabled: getSettings().codeEditingEnabled, + }; + }, + [ clientId ] + ); + const { toggleBlockMode } = useDispatch( blockEditorStore ); + if ( ! blockType || ! hasBlockSupport( blockType, 'html', true ) || @@ -32,26 +41,14 @@ export function BlockModeToggle( { const label = mode === 'visual' ? __( 'Edit as HTML' ) : __( 'Edit visually' ); - return { ! small && label }; + return ( + { + toggleBlockMode( clientId ); + onToggle(); + } } + > + { label } + + ); } - -export default compose( [ - withSelect( ( select, { clientId } ) => { - const { getBlock, getBlockMode, getSettings } = - select( blockEditorStore ); - const block = getBlock( clientId ); - const isCodeEditingEnabled = getSettings().codeEditingEnabled; - - return { - mode: getBlockMode( clientId ), - blockType: block ? getBlockType( block.name ) : null, - isCodeEditingEnabled, - }; - } ), - withDispatch( ( dispatch, { onToggle = noop, clientId } ) => ( { - onToggleMode() { - dispatch( blockEditorStore ).toggleBlockMode( clientId ); - onToggle(); - }, - } ) ), -] )( BlockModeToggle ); diff --git a/packages/block-editor/src/components/block-settings-menu/test/block-mode-toggle.js b/packages/block-editor/src/components/block-settings-menu/test/block-mode-toggle.js index c297bad15f29ea..67d88125e3429c 100644 --- a/packages/block-editor/src/components/block-settings-menu/test/block-mode-toggle.js +++ b/packages/block-editor/src/components/block-settings-menu/test/block-mode-toggle.js @@ -3,16 +3,32 @@ */ import { render, screen } from '@testing-library/react'; +/** + * WordPress dependencies + */ +import { useSelect } from '@wordpress/data'; + /** * Internal dependencies */ -import { BlockModeToggle } from '../block-mode-toggle'; +import BlockModeToggle from '../block-mode-toggle'; + +jest.mock( '@wordpress/data/src/components/use-select', () => jest.fn() ); + +function setupUseSelectMock( mode, blockType, codeEditingEnabled = true ) { + useSelect.mockImplementation( () => { + return { + mode, + blockType, + isCodeEditingEnabled: codeEditingEnabled, + }; + } ); +} describe( 'BlockModeToggle', () => { it( "should not render the HTML mode button if the block doesn't support it", () => { - render( - - ); + setupUseSelectMock( undefined, { supports: { html: false } } ); + render( ); expect( screen.queryByRole( 'menuitem', { name: 'Edit as HTML' } ) @@ -20,12 +36,8 @@ describe( 'BlockModeToggle', () => { } ); it( 'should render the HTML mode button', () => { - render( - - ); + setupUseSelectMock( 'visual', { supports: { html: true } } ); + render( ); expect( screen.getByRole( 'menuitem', { name: 'Edit as HTML' } ) @@ -33,12 +45,8 @@ describe( 'BlockModeToggle', () => { } ); it( 'should render the Visual mode button', () => { - render( - - ); + setupUseSelectMock( 'html', { supports: { html: true } } ); + render( ); expect( screen.getByRole( 'menuitem', { name: 'Edit visually' } ) @@ -46,13 +54,8 @@ describe( 'BlockModeToggle', () => { } ); it( 'should not render the Visual mode button if code editing is disabled', () => { - render( - - ); + setupUseSelectMock( 'html', { supports: { html: true } }, false ); + render( ); expect( screen.queryByRole( 'menuitem', { name: 'Edit visually' } ) From 6445ff13273b7bdbf5315c1394b5aeda8011e47c Mon Sep 17 00:00:00 2001 From: Robert Lee Date: Mon, 12 Aug 2024 22:46:36 -0700 Subject: [PATCH 052/126] WPCompleter: Restrict block list to allowed blocks only (#64413) Unlinked contributors: ssang. Co-authored-by: talldan --- packages/block-editor/src/autocompleters/block.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/block-editor/src/autocompleters/block.js b/packages/block-editor/src/autocompleters/block.js index bc06c9de5aaaff..859ae11036c82a 100644 --- a/packages/block-editor/src/autocompleters/block.js +++ b/packages/block-editor/src/autocompleters/block.js @@ -60,7 +60,8 @@ function createBlockCompleter() { }, [] ); const [ items, categories, collections ] = useBlockTypesState( rootClientId, - noop + noop, + true ); const filteredItems = useMemo( () => { From 476c78b36e0ac1af0f1cbec75507a6b288c4df29 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Tue, 13 Aug 2024 14:15:23 +0800 Subject: [PATCH 053/126] Fix bumped specificity for layout styles in non-iframed editor (#64076) * Fix too specific layout styles in non-iframed editor * Ensure first/last child rules take precedence * Adjust selectors so that `> :first-child`/`> :last-child` still has 0,2,0 specificity to override theme.json spacing * Update tests * Update client side layout selectors to match theme json * Add backport changelog ---- Co-authored-by: talldan Co-authored-by: andrewserong Co-authored-by: ramonjd Co-authored-by: aaronrobertshaw --- backport-changelog/6.6/7145.md | 3 +++ lib/class-wp-theme-json-gutenberg.php | 2 +- .../global-styles/test/use-global-styles-output.js | 6 +++--- .../global-styles/use-global-styles-output.js | 4 ++-- phpunit/class-wp-theme-json-test.php | 11 +++++------ 5 files changed, 14 insertions(+), 12 deletions(-) create mode 100644 backport-changelog/6.6/7145.md diff --git a/backport-changelog/6.6/7145.md b/backport-changelog/6.6/7145.md new file mode 100644 index 00000000000000..386f765cb22fa8 --- /dev/null +++ b/backport-changelog/6.6/7145.md @@ -0,0 +1,3 @@ +https://github.com/WordPress/wordpress-develop/pull/7145 + +* https://github.com/WordPress/gutenberg/pull/64076 diff --git a/lib/class-wp-theme-json-gutenberg.php b/lib/class-wp-theme-json-gutenberg.php index 7558438dcbe745..20ea31090407b4 100644 --- a/lib/class-wp-theme-json-gutenberg.php +++ b/lib/class-wp-theme-json-gutenberg.php @@ -1744,7 +1744,7 @@ protected function get_layout_styles( $block_metadata, $types = array() ) { $spacing_rule['selector'] ); } else { - $format = static::ROOT_BLOCK_SELECTOR === $selector ? '.%2$s %3$s' : '%1$s-%2$s %3$s'; + $format = static::ROOT_BLOCK_SELECTOR === $selector ? ':root :where(.%2$s)%3$s' : ':root :where(%1$s-%2$s)%3$s'; $layout_selector = sprintf( $format, $selector, diff --git a/packages/block-editor/src/components/global-styles/test/use-global-styles-output.js b/packages/block-editor/src/components/global-styles/test/use-global-styles-output.js index e2530fdb85f812..1b061f6921f2c2 100644 --- a/packages/block-editor/src/components/global-styles/test/use-global-styles-output.js +++ b/packages/block-editor/src/components/global-styles/test/use-global-styles-output.js @@ -763,7 +763,7 @@ describe( 'global styles renderer', () => { } ); expect( layoutStyles ).toEqual( - '.is-layout-flow > * { margin-block-start: 0; margin-block-end: 0; }.is-layout-flow > * + * { margin-block-start: 0.5em; margin-block-end: 0; }.is-layout-flex { gap: 0.5em; }:root { --wp--style--block-gap: 0.5em; }.is-layout-flow > .alignleft { float: left; margin-inline-start: 0; margin-inline-end: 2em; }.is-layout-flow > .alignright { float: right; margin-inline-start: 2em; margin-inline-end: 0; }.is-layout-flow > .aligncenter { margin-left: auto !important; margin-right: auto !important; }body .is-layout-flex { display:flex; }.is-layout-flex { flex-wrap: wrap; align-items: center; }.is-layout-flex > * { margin: 0; }' + ':root :where(.is-layout-flow) > * { margin-block-start: 0; margin-block-end: 0; }:root :where(.is-layout-flow) > * + * { margin-block-start: 0.5em; margin-block-end: 0; }:root :where(.is-layout-flex) { gap: 0.5em; }:root { --wp--style--block-gap: 0.5em; }.is-layout-flow > .alignleft { float: left; margin-inline-start: 0; margin-inline-end: 2em; }.is-layout-flow > .alignright { float: right; margin-inline-start: 2em; margin-inline-end: 0; }.is-layout-flow > .aligncenter { margin-left: auto !important; margin-right: auto !important; }body .is-layout-flex { display:flex; }.is-layout-flex { flex-wrap: wrap; align-items: center; }.is-layout-flex > * { margin: 0; }' ); } ); @@ -780,7 +780,7 @@ describe( 'global styles renderer', () => { } ); expect( layoutStyles ).toEqual( - '.is-layout-flow > * { margin-block-start: 0; margin-block-end: 0; }.is-layout-flow > * + * { margin-block-start: 12px; margin-block-end: 0; }.is-layout-flex { gap: 12px; }:root { --wp--style--block-gap: 12px; }.is-layout-flow > .alignleft { float: left; margin-inline-start: 0; margin-inline-end: 2em; }.is-layout-flow > .alignright { float: right; margin-inline-start: 2em; margin-inline-end: 0; }.is-layout-flow > .aligncenter { margin-left: auto !important; margin-right: auto !important; }body .is-layout-flex { display:flex; }.is-layout-flex { flex-wrap: wrap; align-items: center; }.is-layout-flex > * { margin: 0; }' + ':root :where(.is-layout-flow) > * { margin-block-start: 0; margin-block-end: 0; }:root :where(.is-layout-flow) > * + * { margin-block-start: 12px; margin-block-end: 0; }:root :where(.is-layout-flex) { gap: 12px; }:root { --wp--style--block-gap: 12px; }.is-layout-flow > .alignleft { float: left; margin-inline-start: 0; margin-inline-end: 2em; }.is-layout-flow > .alignright { float: right; margin-inline-start: 2em; margin-inline-end: 0; }.is-layout-flow > .aligncenter { margin-left: auto !important; margin-right: auto !important; }body .is-layout-flex { display:flex; }.is-layout-flex { flex-wrap: wrap; align-items: center; }.is-layout-flex > * { margin: 0; }' ); } ); @@ -797,7 +797,7 @@ describe( 'global styles renderer', () => { } ); expect( layoutStyles ).toEqual( - '.wp-block-group-is-layout-flow > * { margin-block-start: 0; margin-block-end: 0; }.wp-block-group-is-layout-flow > * + * { margin-block-start: 12px; margin-block-end: 0; }.wp-block-group-is-layout-flex { gap: 12px; }' + ':root :where(.wp-block-group-is-layout-flow) > * { margin-block-start: 0; margin-block-end: 0; }:root :where(.wp-block-group-is-layout-flow) > * + * { margin-block-start: 12px; margin-block-end: 0; }:root :where(.wp-block-group-is-layout-flex) { gap: 12px; }' ); } ); diff --git a/packages/block-editor/src/components/global-styles/use-global-styles-output.js b/packages/block-editor/src/components/global-styles/use-global-styles-output.js index 9190733d5b6607..c1449d6d8b298f 100644 --- a/packages/block-editor/src/components/global-styles/use-global-styles-output.js +++ b/packages/block-editor/src/components/global-styles/use-global-styles-output.js @@ -538,10 +538,10 @@ export function getLayoutStyles( { } else { combinedSelector = selector === ROOT_BLOCK_SELECTOR - ? `.${ className }${ + ? `:root :where(.${ className })${ spacingStyle?.selector || '' }` - : `${ selector }-${ className }${ + : `:root :where(${ selector }-${ className })${ spacingStyle?.selector || '' }`; } diff --git a/phpunit/class-wp-theme-json-test.php b/phpunit/class-wp-theme-json-test.php index d366e0df44c4a2..3f11dd97a6688f 100644 --- a/phpunit/class-wp-theme-json-test.php +++ b/phpunit/class-wp-theme-json-test.php @@ -714,7 +714,7 @@ public function test_get_stylesheet_renders_enabled_protected_properties() { ) ); - $expected = ':where(body) { margin: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }:where(.wp-site-blocks) > * { margin-block-start: 1em; margin-block-end: 0; }:where(.wp-site-blocks) > :first-child { margin-block-start: 0; }:where(.wp-site-blocks) > :last-child { margin-block-end: 0; }:root { --wp--style--block-gap: 1em; }.is-layout-flow > :first-child{margin-block-start: 0;}.is-layout-flow > :last-child{margin-block-end: 0;}.is-layout-flow > *{margin-block-start: 1em;margin-block-end: 0;}.is-layout-constrained > :first-child{margin-block-start: 0;}.is-layout-constrained > :last-child{margin-block-end: 0;}.is-layout-constrained > *{margin-block-start: 1em;margin-block-end: 0;}.is-layout-flex {gap: 1em;}.is-layout-grid {gap: 1em;}.is-layout-flow > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-flow > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-flow > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-constrained > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-constrained > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > :where(:not(.alignleft):not(.alignright):not(.alignfull)){margin-left: auto !important;margin-right: auto !important;}body .is-layout-flex{display: flex;}.is-layout-flex{flex-wrap: wrap;align-items: center;}.is-layout-flex > :is(*, div){margin: 0;}body .is-layout-grid{display: grid;}.is-layout-grid > :is(*, div){margin: 0;}'; + $expected = ':where(body) { margin: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }:where(.wp-site-blocks) > * { margin-block-start: 1em; margin-block-end: 0; }:where(.wp-site-blocks) > :first-child { margin-block-start: 0; }:where(.wp-site-blocks) > :last-child { margin-block-end: 0; }:root { --wp--style--block-gap: 1em; }:root :where(.is-layout-flow) > :first-child{margin-block-start: 0;}:root :where(.is-layout-flow) > :last-child{margin-block-end: 0;}:root :where(.is-layout-flow) > *{margin-block-start: 1em;margin-block-end: 0;}:root :where(.is-layout-constrained) > :first-child{margin-block-start: 0;}:root :where(.is-layout-constrained) > :last-child{margin-block-end: 0;}:root :where(.is-layout-constrained) > *{margin-block-start: 1em;margin-block-end: 0;}:root :where(.is-layout-flex){gap: 1em;}:root :where(.is-layout-grid){gap: 1em;}.is-layout-flow > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-flow > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-flow > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-constrained > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-constrained > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > :where(:not(.alignleft):not(.alignright):not(.alignfull)){margin-left: auto !important;margin-right: auto !important;}body .is-layout-flex{display: flex;}.is-layout-flex{flex-wrap: wrap;align-items: center;}.is-layout-flex > :is(*, div){margin: 0;}body .is-layout-grid{display: grid;}.is-layout-grid > :is(*, div){margin: 0;}'; $this->assertSameCSS( $expected, $theme_json->get_stylesheet() ); $this->assertSameCSS( $expected, $theme_json->get_stylesheet( array( 'styles' ) ) ); } @@ -1131,7 +1131,7 @@ public function test_get_stylesheet_generates_layout_styles() { // Results also include root site blocks styles. $this->assertSameCSS( - ':root { --wp--style--global--content-size: 640px;--wp--style--global--wide-size: 1200px; }:where(body) { margin: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }:where(.wp-site-blocks) > * { margin-block-start: 1em; margin-block-end: 0; }:where(.wp-site-blocks) > :first-child { margin-block-start: 0; }:where(.wp-site-blocks) > :last-child { margin-block-end: 0; }:root { --wp--style--block-gap: 1em; }.is-layout-flow > :first-child{margin-block-start: 0;}.is-layout-flow > :last-child{margin-block-end: 0;}.is-layout-flow > *{margin-block-start: 1em;margin-block-end: 0;}.is-layout-constrained > :first-child{margin-block-start: 0;}.is-layout-constrained > :last-child{margin-block-end: 0;}.is-layout-constrained > *{margin-block-start: 1em;margin-block-end: 0;}.is-layout-flex {gap: 1em;}.is-layout-grid {gap: 1em;}.is-layout-flow > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-flow > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-flow > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-constrained > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-constrained > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > :where(:not(.alignleft):not(.alignright):not(.alignfull)){max-width: var(--wp--style--global--content-size);margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > .alignwide{max-width: var(--wp--style--global--wide-size);}body .is-layout-flex{display: flex;}.is-layout-flex{flex-wrap: wrap;align-items: center;}.is-layout-flex > :is(*, div){margin: 0;}body .is-layout-grid{display: grid;}.is-layout-grid > :is(*, div){margin: 0;}', + ':root { --wp--style--global--content-size: 640px;--wp--style--global--wide-size: 1200px; }:where(body) { margin: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }:where(.wp-site-blocks) > * { margin-block-start: 1em; margin-block-end: 0; }:where(.wp-site-blocks) > :first-child { margin-block-start: 0; }:where(.wp-site-blocks) > :last-child { margin-block-end: 0; }:root { --wp--style--block-gap: 1em; }:root :where(.is-layout-flow) > :first-child{margin-block-start: 0;}:root :where(.is-layout-flow) > :last-child{margin-block-end: 0;}:root :where(.is-layout-flow) > *{margin-block-start: 1em;margin-block-end: 0;}:root :where(.is-layout-constrained) > :first-child{margin-block-start: 0;}:root :where(.is-layout-constrained) > :last-child{margin-block-end: 0;}:root :where(.is-layout-constrained) > *{margin-block-start: 1em;margin-block-end: 0;}:root :where(.is-layout-flex){gap: 1em;}:root :where(.is-layout-grid){gap: 1em;}.is-layout-flow > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-flow > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-flow > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-constrained > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-constrained > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > :where(:not(.alignleft):not(.alignright):not(.alignfull)){max-width: var(--wp--style--global--content-size);margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > .alignwide{max-width: var(--wp--style--global--wide-size);}body .is-layout-flex{display: flex;}.is-layout-flex{flex-wrap: wrap;align-items: center;}.is-layout-flex > :is(*, div){margin: 0;}body .is-layout-grid{display: grid;}.is-layout-grid > :is(*, div){margin: 0;}', $theme_json->get_stylesheet( array( 'styles' ) ) ); } @@ -1160,7 +1160,7 @@ public function test_get_stylesheet_generates_layout_styles_with_spacing_presets // Results also include root site blocks styles. $this->assertSameCSS( - ':root { --wp--style--global--content-size: 640px;--wp--style--global--wide-size: 1200px; }:where(body) { margin: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }:where(.wp-site-blocks) > * { margin-block-start: var(--wp--preset--spacing--60); margin-block-end: 0; }:where(.wp-site-blocks) > :first-child { margin-block-start: 0; }:where(.wp-site-blocks) > :last-child { margin-block-end: 0; }:root { --wp--style--block-gap: var(--wp--preset--spacing--60); }.is-layout-flow > :first-child{margin-block-start: 0;}.is-layout-flow > :last-child{margin-block-end: 0;}.is-layout-flow > *{margin-block-start: var(--wp--preset--spacing--60);margin-block-end: 0;}.is-layout-constrained > :first-child{margin-block-start: 0;}.is-layout-constrained > :last-child{margin-block-end: 0;}.is-layout-constrained > *{margin-block-start: var(--wp--preset--spacing--60);margin-block-end: 0;}.is-layout-flex {gap: var(--wp--preset--spacing--60);}.is-layout-grid {gap: var(--wp--preset--spacing--60);}.is-layout-flow > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-flow > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-flow > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-constrained > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-constrained > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > :where(:not(.alignleft):not(.alignright):not(.alignfull)){max-width: var(--wp--style--global--content-size);margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > .alignwide{max-width: var(--wp--style--global--wide-size);}body .is-layout-flex{display: flex;}.is-layout-flex{flex-wrap: wrap;align-items: center;}.is-layout-flex > :is(*, div){margin: 0;}body .is-layout-grid{display: grid;}.is-layout-grid > :is(*, div){margin: 0;}', + ':root { --wp--style--global--content-size: 640px;--wp--style--global--wide-size: 1200px; }:where(body) { margin: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }:where(.wp-site-blocks) > * { margin-block-start: var(--wp--preset--spacing--60); margin-block-end: 0; }:where(.wp-site-blocks) > :first-child { margin-block-start: 0; }:where(.wp-site-blocks) > :last-child { margin-block-end: 0; }:root { --wp--style--block-gap: var(--wp--preset--spacing--60); }:root :where(.is-layout-flow) > :first-child{margin-block-start: 0;}:root :where(.is-layout-flow) > :last-child{margin-block-end: 0;}:root :where(.is-layout-flow) > *{margin-block-start: var(--wp--preset--spacing--60);margin-block-end: 0;}:root :where(.is-layout-constrained) > :first-child{margin-block-start: 0;}:root :where(.is-layout-constrained) > :last-child{margin-block-end: 0;}:root :where(.is-layout-constrained) > *{margin-block-start: var(--wp--preset--spacing--60);margin-block-end: 0;}:root :where(.is-layout-flex){gap: var(--wp--preset--spacing--60);}:root :where(.is-layout-grid){gap: var(--wp--preset--spacing--60);}.is-layout-flow > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-flow > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-flow > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-constrained > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-constrained > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > :where(:not(.alignleft):not(.alignright):not(.alignfull)){max-width: var(--wp--style--global--content-size);margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > .alignwide{max-width: var(--wp--style--global--wide-size);}body .is-layout-flex{display: flex;}.is-layout-flex{flex-wrap: wrap;align-items: center;}.is-layout-flex > :is(*, div){margin: 0;}body .is-layout-grid{display: grid;}.is-layout-grid > :is(*, div){margin: 0;}', $theme_json->get_stylesheet( array( 'styles' ) ) ); } @@ -1284,8 +1284,7 @@ public function test_get_stylesheet_generates_valid_block_gap_values_and_skips_n ); $this->assertSameCSS( - ':root { --wp--style--global--content-size: 640px;--wp--style--global--wide-size: 1200px; }:where(body) { margin: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }:where(.wp-site-blocks) > * { margin-block-start: 1rem; margin-block-end: 0; }:where(.wp-site-blocks) > :first-child { margin-block-start: 0; }:where(.wp-site-blocks) > :last-child { margin-block-end: 0; }:root { --wp--style--block-gap: 1rem; }.is-layout-flow > :first-child{margin-block-start: 0;}.is-layout-flow > :last-child{margin-block-end: 0;}.is-layout-flow > *{margin-block-start: 1rem;margin-block-end: 0;}.is-layout-constrained > :first-child{margin-block-start: 0;}.is-layout-constrained > :last-child{margin-block-end: 0;}.is-layout-constrained > *{margin-block-start: 1rem;margin-block-end: 0;}.is-layout-flex {gap: 1rem;}.is-layout-grid {gap: 1rem;}.is-layout-flow > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-flow > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-flow > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-constrained > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-constrained > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > :where(:not(.alignleft):not(.alignright):not(.alignfull)){max-width: var(--wp--style--global--content-size);margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > .alignwide{max-width: var(--wp--style--global--wide-size);}body .is-layout-flex{display: flex;}.is-layout-flex{flex-wrap: wrap;align-items: center;}.is-layout-flex > :is(*, div){margin: 0;}body .is-layout-grid{display: grid;}.is-layout-grid > :is(*, div){margin: 0;}' . - ':root :where(.wp-block-post-content){color: gray;}.wp-block-social-links-is-layout-flow > :first-child{margin-block-start: 0;}.wp-block-social-links-is-layout-flow > :last-child{margin-block-end: 0;}.wp-block-social-links-is-layout-flow > *{margin-block-start: 0;margin-block-end: 0;}.wp-block-social-links-is-layout-constrained > :first-child{margin-block-start: 0;}.wp-block-social-links-is-layout-constrained > :last-child{margin-block-end: 0;}.wp-block-social-links-is-layout-constrained > *{margin-block-start: 0;margin-block-end: 0;}.wp-block-social-links-is-layout-flex {gap: 0;}.wp-block-social-links-is-layout-grid {gap: 0;}.wp-block-buttons-is-layout-flow > :first-child{margin-block-start: 0;}.wp-block-buttons-is-layout-flow > :last-child{margin-block-end: 0;}.wp-block-buttons-is-layout-flow > *{margin-block-start: 0;margin-block-end: 0;}.wp-block-buttons-is-layout-constrained > :first-child{margin-block-start: 0;}.wp-block-buttons-is-layout-constrained > :last-child{margin-block-end: 0;}.wp-block-buttons-is-layout-constrained > *{margin-block-start: 0;margin-block-end: 0;}.wp-block-buttons-is-layout-flex {gap: 0;}.wp-block-buttons-is-layout-grid {gap: 0;}', + ':root { --wp--style--global--content-size: 640px;--wp--style--global--wide-size: 1200px; }:where(body) { margin: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }:where(.wp-site-blocks) > * { margin-block-start: 1rem; margin-block-end: 0; }:where(.wp-site-blocks) > :first-child { margin-block-start: 0; }:where(.wp-site-blocks) > :last-child { margin-block-end: 0; }:root { --wp--style--block-gap: 1rem; }:root :where(.is-layout-flow) > :first-child{margin-block-start: 0;}:root :where(.is-layout-flow) > :last-child{margin-block-end: 0;}:root :where(.is-layout-flow) > *{margin-block-start: 1rem;margin-block-end: 0;}:root :where(.is-layout-constrained) > :first-child{margin-block-start: 0;}:root :where(.is-layout-constrained) > :last-child{margin-block-end: 0;}:root :where(.is-layout-constrained) > *{margin-block-start: 1rem;margin-block-end: 0;}:root :where(.is-layout-flex){gap: 1rem;}:root :where(.is-layout-grid){gap: 1rem;}.is-layout-flow > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-flow > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-flow > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-constrained > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-constrained > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > :where(:not(.alignleft):not(.alignright):not(.alignfull)){max-width: var(--wp--style--global--content-size);margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > .alignwide{max-width: var(--wp--style--global--wide-size);}body .is-layout-flex{display: flex;}.is-layout-flex{flex-wrap: wrap;align-items: center;}.is-layout-flex > :is(*, div){margin: 0;}body .is-layout-grid{display: grid;}.is-layout-grid > :is(*, div){margin: 0;}:root :where(.wp-block-post-content){color: gray;}:root :where(.wp-block-social-links-is-layout-flow) > :first-child{margin-block-start: 0;}:root :where(.wp-block-social-links-is-layout-flow) > :last-child{margin-block-end: 0;}:root :where(.wp-block-social-links-is-layout-flow) > *{margin-block-start: 0;margin-block-end: 0;}:root :where(.wp-block-social-links-is-layout-constrained) > :first-child{margin-block-start: 0;}:root :where(.wp-block-social-links-is-layout-constrained) > :last-child{margin-block-end: 0;}:root :where(.wp-block-social-links-is-layout-constrained) > *{margin-block-start: 0;margin-block-end: 0;}:root :where(.wp-block-social-links-is-layout-flex){gap: 0;}:root :where(.wp-block-social-links-is-layout-grid){gap: 0;}:root :where(.wp-block-buttons-is-layout-flow) > :first-child{margin-block-start: 0;}:root :where(.wp-block-buttons-is-layout-flow) > :last-child{margin-block-end: 0;}:root :where(.wp-block-buttons-is-layout-flow) > *{margin-block-start: 0;margin-block-end: 0;}:root :where(.wp-block-buttons-is-layout-constrained) > :first-child{margin-block-start: 0;}:root :where(.wp-block-buttons-is-layout-constrained) > :last-child{margin-block-end: 0;}:root :where(.wp-block-buttons-is-layout-constrained) > *{margin-block-start: 0;margin-block-end: 0;}:root :where(.wp-block-buttons-is-layout-flex){gap: 0;}:root :where(.wp-block-buttons-is-layout-grid){gap: 0;}', $theme_json->get_stylesheet() ); } @@ -3690,7 +3689,7 @@ public function test_get_styles_with_appearance_tools() { 'selector' => 'body', ); - $expected = ':where(body) { margin: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }:where(.wp-site-blocks) > * { margin-block-start: ; margin-block-end: 0; }:where(.wp-site-blocks) > :first-child { margin-block-start: 0; }:where(.wp-site-blocks) > :last-child { margin-block-end: 0; }:root { --wp--style--block-gap: ; }.is-layout-flow > :first-child{margin-block-start: 0;}.is-layout-flow > :last-child{margin-block-end: 0;}.is-layout-flow > *{margin-block-start: 1;margin-block-end: 0;}.is-layout-constrained > :first-child{margin-block-start: 0;}.is-layout-constrained > :last-child{margin-block-end: 0;}.is-layout-constrained > *{margin-block-start: 1;margin-block-end: 0;}.is-layout-flex {gap: 1;}.is-layout-grid {gap: 1;}.is-layout-flow > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-flow > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-flow > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-constrained > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-constrained > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > :where(:not(.alignleft):not(.alignright):not(.alignfull)){margin-left: auto !important;margin-right: auto !important;}body .is-layout-flex{display: flex;}.is-layout-flex{flex-wrap: wrap;align-items: center;}.is-layout-flex > :is(*, div){margin: 0;}body .is-layout-grid{display: grid;}.is-layout-grid > :is(*, div){margin: 0;}'; + $expected = ':where(body) { margin: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }:where(.wp-site-blocks) > * { margin-block-start: ; margin-block-end: 0; }:where(.wp-site-blocks) > :first-child { margin-block-start: 0; }:where(.wp-site-blocks) > :last-child { margin-block-end: 0; }:root { --wp--style--block-gap: ; }:root :where(.is-layout-flow) > :first-child{margin-block-start: 0;}:root :where(.is-layout-flow) > :last-child{margin-block-end: 0;}:root :where(.is-layout-flow) > *{margin-block-start: 1;margin-block-end: 0;}:root :where(.is-layout-constrained) > :first-child{margin-block-start: 0;}:root :where(.is-layout-constrained) > :last-child{margin-block-end: 0;}:root :where(.is-layout-constrained) > *{margin-block-start: 1;margin-block-end: 0;}:root :where(.is-layout-flex){gap: 1;}:root :where(.is-layout-grid){gap: 1;}.is-layout-flow > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-flow > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-flow > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-constrained > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-constrained > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > :where(:not(.alignleft):not(.alignright):not(.alignfull)){margin-left: auto !important;margin-right: auto !important;}body .is-layout-flex{display: flex;}.is-layout-flex{flex-wrap: wrap;align-items: center;}.is-layout-flex > :is(*, div){margin: 0;}body .is-layout-grid{display: grid;}.is-layout-grid > :is(*, div){margin: 0;}'; $this->assertSameCSS( $expected, $theme_json->get_root_layout_rules( WP_Theme_JSON_Gutenberg::ROOT_BLOCK_SELECTOR, $metadata ) ); } From 33093652ea76c4463e78252bb53f2f331a81610c Mon Sep 17 00:00:00 2001 From: Dave Smith Date: Tue, 13 Aug 2024 08:29:48 +0100 Subject: [PATCH 054/126] Focus pattern inserter search when activating zoom out inserter (#64396) * Add new search focus option for inserter panel * Try focusing when inserter clicked in Zoom Out * Store search input ref in block editor settings * Remove legacy selector * Remove more legacy code * Move all ref selectors to block editor * Update packages/editor/src/components/inserter-sidebar/index.js --------- Co-authored-by: Ben Dwyer Co-authored-by: getdave Co-authored-by: scruffian --- .../block-tools/zoom-out-mode-inserters.js | 8 ++++- .../src/components/inserter/library.js | 2 ++ .../src/components/inserter/menu.js | 34 ++++++++++++------- .../src/store/private-selectors.js | 4 +++ packages/block-editor/src/store/reducer.js | 5 +++ 5 files changed, 39 insertions(+), 14 deletions(-) diff --git a/packages/block-editor/src/components/block-tools/zoom-out-mode-inserters.js b/packages/block-editor/src/components/block-tools/zoom-out-mode-inserters.js index bb044f9479c024..6f986ce90dc3bd 100644 --- a/packages/block-editor/src/components/block-tools/zoom-out-mode-inserters.js +++ b/packages/block-editor/src/components/block-tools/zoom-out-mode-inserters.js @@ -23,6 +23,7 @@ function ZoomOutModeInserters() { sectionRootClientId, selectedBlockClientId, hoveredBlockClientId, + inserterSearchInputRef, } = useSelect( ( select ) => { const { getSettings, @@ -32,8 +33,11 @@ function ZoomOutModeInserters() { getSelectedBlockClientId, getHoveredBlockClientId, isBlockInsertionPointVisible, - } = select( blockEditorStore ); + getInserterSearchInputRef, + } = unlock( select( blockEditorStore ) ); + const { sectionRootClientId: root } = unlock( getSettings() ); + return { hasSelection: !! getSelectionStart().clientId, blockInsertionPoint: getBlockInsertionPoint(), @@ -44,6 +48,7 @@ function ZoomOutModeInserters() { getSettings().__experimentalSetIsInserterOpened, selectedBlockClientId: getSelectedBlockClientId(), hoveredBlockClientId: getHoveredBlockClientId(), + inserterSearchInputRef: getInserterSearchInputRef(), }; }, [] ); @@ -110,6 +115,7 @@ function ZoomOutModeInserters() { showInsertionPoint( sectionRootClientId, index, { operation: 'insert', } ); + inserterSearchInputRef?.current?.focus(); } } /> ) } diff --git a/packages/block-editor/src/components/inserter/library.js b/packages/block-editor/src/components/inserter/library.js index 4e10a051996a9f..fe14d48bb4016b 100644 --- a/packages/block-editor/src/components/inserter/library.js +++ b/packages/block-editor/src/components/inserter/library.js @@ -27,6 +27,7 @@ function InserterLibrary( onSelect = noop, shouldFocusBlock = false, onClose, + __experimentalSearchInputRef, }, ref ) { @@ -58,6 +59,7 @@ function InserterLibrary( shouldFocusBlock={ shouldFocusBlock } ref={ ref } onClose={ onClose } + __experimentalSearchInputRef={ __experimentalSearchInputRef } /> ); } diff --git a/packages/block-editor/src/components/inserter/menu.js b/packages/block-editor/src/components/inserter/menu.js index 52e4e3062e9024..c5f41f9e3bf0a0 100644 --- a/packages/block-editor/src/components/inserter/menu.js +++ b/packages/block-editor/src/components/inserter/menu.js @@ -33,6 +33,7 @@ import useInsertionPoint from './hooks/use-insertion-point'; import { store as blockEditorStore } from '../../store'; import TabbedSidebar from '../tabbed-sidebar'; import { useZoomOut } from '../../hooks/use-zoom-out'; +import { unlock } from '../../lock-unlock'; const NOOP = () => {}; function InserterMenu( @@ -53,11 +54,16 @@ function InserterMenu( }, ref ) { - const isZoomOutMode = useSelect( - ( select ) => - select( blockEditorStore ).__unstableGetEditorMode() === 'zoom-out', - [] - ); + const { isZoomOutMode, inserterSearchInputRef } = useSelect( ( select ) => { + const { __unstableGetEditorMode, getInserterSearchInputRef } = unlock( + select( blockEditorStore ) + ); + return { + isZoomOutMode: __unstableGetEditorMode() === 'zoom-out', + inserterSearchInputRef: getInserterSearchInputRef(), + }; + }, [] ); + const [ filterValue, setFilterValue, delayedFilterValue ] = useDebouncedInput( __experimentalFilterValue ); const [ hoveredItem, setHoveredItem ] = useState( null ); @@ -104,7 +110,7 @@ function InserterMenu( } } ); }, - [ onInsertBlocks, onSelect, shouldFocusBlock ] + [ onInsertBlocks, onSelect, ref, shouldFocusBlock ] ); const onInsertPattern = useCallback( @@ -113,7 +119,7 @@ function InserterMenu( onInsertBlocks( blocks, { patternName } ); onSelect(); }, - [ onInsertBlocks, onSelect ] + [ onInsertBlocks, onSelect, onToggleInsertionPoint ] ); const onHover = useCallback( @@ -164,7 +170,9 @@ function InserterMenu( value={ filterValue } label={ __( 'Search for blocks and patterns' ) } placeholder={ __( 'Search' ) } + ref={ inserterSearchInputRef } /> + { !! delayedFilterValue && ( { diff --git a/packages/block-editor/src/store/private-selectors.js b/packages/block-editor/src/store/private-selectors.js index c534c65b8defe7..dcd315a0ae2804 100644 --- a/packages/block-editor/src/store/private-selectors.js +++ b/packages/block-editor/src/store/private-selectors.js @@ -511,3 +511,7 @@ export function getTemporarilyEditingAsBlocks( state ) { export function getTemporarilyEditingFocusModeToRevert( state ) { return state.temporarilyEditingFocusModeRevert; } + +export function getInserterSearchInputRef( state ) { + return state.inserterSearchInputRef; +} diff --git a/packages/block-editor/src/store/reducer.js b/packages/block-editor/src/store/reducer.js index 91d853a50a6a42..d9352670776371 100644 --- a/packages/block-editor/src/store/reducer.js +++ b/packages/block-editor/src/store/reducer.js @@ -2087,6 +2087,10 @@ export function hoveredBlockClientId( state = false, action ) { return state; } +export function inserterSearchInputRef( state = { current: null } ) { + return state; +} + const combinedReducers = combineReducers( { blocks, isDragging, @@ -2120,6 +2124,7 @@ const combinedReducers = combineReducers( { openedBlockSettingsMenu, registeredInserterMediaCategories, hoveredBlockClientId, + inserterSearchInputRef, } ); function withAutomaticChangeReset( reducer ) { From 27e441209db11f384e484f5a71453143c64c9b4a Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Tue, 13 Aug 2024 16:00:20 +0800 Subject: [PATCH 055/126] Update postcss-prefixwrap dependency to 1.51.0 to fix prefixing in `:where` selectors (#64458) * Update postcss-prefixwrap dependency to 1.51.0 to fix prefixing in :where selectors * Add extra test for :where with a pseudo selector ---- Co-authored-by: talldan Co-authored-by: aaronrobertshaw Co-authored-by: andrewserong Co-authored-by: andreiglingeanu --- package-lock.json | 31 ++++++------ packages/block-editor/package.json | 2 +- .../src/utils/test/transform-styles.js | 49 +++++++++++++++++++ 3 files changed, 66 insertions(+), 16 deletions(-) diff --git a/package-lock.json b/package-lock.json index 66c32076d3b364..471d7b845d24f8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41754,14 +41754,6 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "dev": true }, - "node_modules/postcss-prefixwrap": { - "version": "1.41.0", - "resolved": "https://registry.npmjs.org/postcss-prefixwrap/-/postcss-prefixwrap-1.41.0.tgz", - "integrity": "sha512-gmwwAEE+ci3/ZKjUZppTETINlh1QwihY8gCstInuS7ohk353KYItU4d64hvnUvO2GUy29hBGPHz4Ce+qJRi90A==", - "peerDependencies": { - "postcss": "*" - } - }, "node_modules/postcss-reduce-initial": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-6.0.0.tgz", @@ -52237,7 +52229,7 @@ "fast-deep-equal": "^3.1.3", "memize": "^2.1.0", "postcss": "^8.4.21", - "postcss-prefixwrap": "^1.41.0", + "postcss-prefixwrap": "^1.51.0", "postcss-urlrebase": "^1.4.0", "react-autosize-textarea": "^7.1.0", "react-easy-crop": "^5.0.6", @@ -52252,6 +52244,15 @@ "react-dom": "^18.0.0" } }, + "packages/block-editor/node_modules/postcss-prefixwrap": { + "version": "1.51.0", + "resolved": "https://registry.npmjs.org/postcss-prefixwrap/-/postcss-prefixwrap-1.51.0.tgz", + "integrity": "sha512-PuP4md5zFSY921dUcLShwSLv2YyyDec0dK9/puXl/lu7ZNvJ1U59+ZEFRMS67xwfNg5nIIlPXnAycPJlhA/Isw==", + "license": "MIT", + "peerDependencies": { + "postcss": "*" + } + }, "packages/block-editor/node_modules/postcss-urlrebase": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/postcss-urlrebase/-/postcss-urlrebase-1.4.0.tgz", @@ -67255,13 +67256,18 @@ "fast-deep-equal": "^3.1.3", "memize": "^2.1.0", "postcss": "^8.4.21", - "postcss-prefixwrap": "^1.41.0", + "postcss-prefixwrap": "^1.51.0", "postcss-urlrebase": "^1.4.0", "react-autosize-textarea": "^7.1.0", "react-easy-crop": "^5.0.6", "remove-accents": "^0.5.0" }, "dependencies": { + "postcss-prefixwrap": { + "version": "1.51.0", + "resolved": "https://registry.npmjs.org/postcss-prefixwrap/-/postcss-prefixwrap-1.51.0.tgz", + "integrity": "sha512-PuP4md5zFSY921dUcLShwSLv2YyyDec0dK9/puXl/lu7ZNvJ1U59+ZEFRMS67xwfNg5nIIlPXnAycPJlhA/Isw==" + }, "postcss-urlrebase": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/postcss-urlrebase/-/postcss-urlrebase-1.4.0.tgz", @@ -87820,11 +87826,6 @@ } } }, - "postcss-prefixwrap": { - "version": "1.41.0", - "resolved": "https://registry.npmjs.org/postcss-prefixwrap/-/postcss-prefixwrap-1.41.0.tgz", - "integrity": "sha512-gmwwAEE+ci3/ZKjUZppTETINlh1QwihY8gCstInuS7ohk353KYItU4d64hvnUvO2GUy29hBGPHz4Ce+qJRi90A==" - }, "postcss-reduce-initial": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-6.0.0.tgz", diff --git a/packages/block-editor/package.json b/packages/block-editor/package.json index 8ccaee6f0a955c..02765376e314b6 100644 --- a/packages/block-editor/package.json +++ b/packages/block-editor/package.json @@ -73,7 +73,7 @@ "fast-deep-equal": "^3.1.3", "memize": "^2.1.0", "postcss": "^8.4.21", - "postcss-prefixwrap": "^1.41.0", + "postcss-prefixwrap": "^1.51.0", "postcss-urlrebase": "^1.4.0", "react-autosize-textarea": "^7.1.0", "react-easy-crop": "^5.0.6", diff --git a/packages/block-editor/src/utils/test/transform-styles.js b/packages/block-editor/src/utils/test/transform-styles.js index 8245ce62831078..b0c6ca48deb355 100644 --- a/packages/block-editor/src/utils/test/transform-styles.js +++ b/packages/block-editor/src/utils/test/transform-styles.js @@ -125,6 +125,21 @@ describe( 'transformStyles', () => { expect( output ).toMatchSnapshot(); } ); + it( `should not try to replace 'body' in the middle of a classname`, () => { + const prefix = '.my-namespace'; + const input = `.has-body-text { color: red; }`; + const output = transformStyles( + [ + { + css: input, + }, + ], + prefix + ); + + expect( output ).toEqual( [ `${ prefix } ${ input }` ] ); + } ); + it( 'should ignore keyframes', () => { const input = ` @keyframes edit-post__fade-in-animation { @@ -210,6 +225,40 @@ describe( 'transformStyles', () => { expect( output ).toMatchSnapshot(); } ); + + it( 'should not try to wrap items within `:where` selectors', () => { + const input = `:where(.wp-element-button:active, .wp-block-button__link:active) { color: blue; }`; + const prefix = '.my-namespace'; + const expected = [ `${ prefix } ${ input }` ]; + + const output = transformStyles( + [ + { + css: input, + }, + ], + prefix + ); + + expect( output ).toEqual( expected ); + } ); + + it( 'should not try to prefix pseudo elements on `:where` selectors', () => { + const input = `:where(.wp-element-button, .wp-block-button__link)::before { color: blue; }`; + const prefix = '.my-namespace'; + const expected = [ `${ prefix } ${ input }` ]; + + const output = transformStyles( + [ + { + css: input, + }, + ], + prefix + ); + + expect( output ).toEqual( expected ); + } ); } ); it( 'should not break with data urls', () => { From 926e7389a95701c3eba4702d4b4ef7f5d4325ffb Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Tue, 13 Aug 2024 10:25:34 +0200 Subject: [PATCH 056/126] DataViews Extensibility: Allow unregistering the duplicate post action (#64441) Co-authored-by: youknowriad Co-authored-by: ntsekouras Co-authored-by: Mamaduka Co-authored-by: sirreal Co-authored-by: jsnajdr --- .../src/components/post-actions/actions.js | 219 +----------------- .../actions/duplicate-post.native.tsx | 3 + .../src/dataviews/actions/duplicate-post.tsx | 174 ++++++++++++++ .../src/dataviews/actions/reorder-page.tsx | 6 +- packages/editor/src/dataviews/fields/index.ts | 7 +- .../src/dataviews/store/private-actions.ts | 9 + packages/editor/src/dataviews/types.ts | 26 ++- 7 files changed, 213 insertions(+), 231 deletions(-) create mode 100644 packages/editor/src/dataviews/actions/duplicate-post.native.tsx create mode 100644 packages/editor/src/dataviews/actions/duplicate-post.tsx diff --git a/packages/editor/src/components/post-actions/actions.js b/packages/editor/src/components/post-actions/actions.js index 0df73d1996deb5..627de5e8652e47 100644 --- a/packages/editor/src/components/post-actions/actions.js +++ b/packages/editor/src/components/post-actions/actions.js @@ -4,50 +4,15 @@ import { external } from '@wordpress/icons'; import { addQueryArgs } from '@wordpress/url'; import { useDispatch, useSelect } from '@wordpress/data'; -import { decodeEntities } from '@wordpress/html-entities'; import { store as coreStore } from '@wordpress/core-data'; -import { __, sprintf, _x } from '@wordpress/i18n'; -import { store as noticesStore } from '@wordpress/notices'; -import { useMemo, useState, useEffect } from '@wordpress/element'; -import { DataForm } from '@wordpress/dataviews'; -import { - Button, - __experimentalHStack as HStack, - __experimentalVStack as VStack, -} from '@wordpress/components'; +import { __, sprintf } from '@wordpress/i18n'; +import { useMemo, useEffect } from '@wordpress/element'; /** * Internal dependencies */ -import { - TEMPLATE_PART_POST_TYPE, - TEMPLATE_POST_TYPE, - PATTERN_POST_TYPE, -} from '../../store/constants'; import { store as editorStore } from '../../store'; import { unlock } from '../../lock-unlock'; -import { getItemTitle } from '../../dataviews/actions/utils'; - -// TODO: this should be shared with other components (see post-fields in edit-site). -const fields = [ - { - type: 'text', - id: 'title', - label: __( 'Title' ), - placeholder: __( 'No title' ), - getValue: ( { item } ) => item.title, - }, - { - type: 'integer', - id: 'menu_order', - label: __( 'Order' ), - description: __( 'Determines the order of pages.' ), - }, -]; - -const formDuplicateAction = { - fields: [ 'title' ], -}; const viewPostAction = { id: 'view-post', @@ -100,172 +65,6 @@ const postRevisionsAction = { }, }; -const useDuplicatePostAction = ( postType ) => { - const userCanCreatePost = useSelect( - ( select ) => { - return select( coreStore ).canUser( 'create', { - kind: 'postType', - name: postType, - } ); - }, - [ postType ] - ); - return useMemo( - () => - userCanCreatePost && { - id: 'duplicate-post', - label: _x( 'Duplicate', 'action label' ), - isEligible( { status } ) { - return status !== 'trash'; - }, - RenderModal: ( { items, closeModal, onActionPerformed } ) => { - const [ item, setItem ] = useState( { - ...items[ 0 ], - title: sprintf( - /* translators: %s: Existing template title */ - __( '%s (Copy)' ), - getItemTitle( items[ 0 ] ) - ), - } ); - - const [ isCreatingPage, setIsCreatingPage ] = - useState( false ); - - const { saveEntityRecord } = useDispatch( coreStore ); - const { createSuccessNotice, createErrorNotice } = - useDispatch( noticesStore ); - - async function createPage( event ) { - event.preventDefault(); - - if ( isCreatingPage ) { - return; - } - - const newItemOject = { - status: 'draft', - title: item.title, - slug: item.title || __( 'No title' ), - comment_status: item.comment_status, - content: - typeof item.content === 'string' - ? item.content - : item.content.raw, - excerpt: item.excerpt.raw, - meta: item.meta, - parent: item.parent, - password: item.password, - template: item.template, - format: item.format, - featured_media: item.featured_media, - menu_order: item.menu_order, - ping_status: item.ping_status, - }; - const assignablePropertiesPrefix = 'wp:action-assign-'; - // Get all the properties that the current user is able to assign normally author, categories, tags, - // and custom taxonomies. - const assignableProperties = Object.keys( - item?._links || {} - ) - .filter( ( property ) => - property.startsWith( - assignablePropertiesPrefix - ) - ) - .map( ( property ) => - property.slice( - assignablePropertiesPrefix.length - ) - ); - assignableProperties.forEach( ( property ) => { - if ( item[ property ] ) { - newItemOject[ property ] = item[ property ]; - } - } ); - setIsCreatingPage( true ); - try { - const newItem = await saveEntityRecord( - 'postType', - item.type, - newItemOject, - { throwOnError: true } - ); - - createSuccessNotice( - sprintf( - // translators: %s: Title of the created template e.g: "Category". - __( '"%s" successfully created.' ), - decodeEntities( - newItem.title?.rendered || item.title - ) - ), - { - id: 'duplicate-post-action', - type: 'snackbar', - } - ); - - if ( onActionPerformed ) { - onActionPerformed( [ newItem ] ); - } - } catch ( error ) { - const errorMessage = - error.message && error.code !== 'unknown_error' - ? error.message - : __( - 'An error occurred while duplicating the page.' - ); - - createErrorNotice( errorMessage, { - type: 'snackbar', - } ); - } finally { - setIsCreatingPage( false ); - closeModal(); - } - } - - return ( -
- - - setItem( { - ...item, - ...changes, - } ) - } - /> - - - - - -
- ); - }, - }, - [ userCanCreatePost ] - ); -}; - export function usePostActions( { postType, onActionPerformed, context } ) { const { defaultActions, postTypeObject } = useSelect( ( select ) => { @@ -284,12 +83,6 @@ export function usePostActions( { postType, onActionPerformed, context } ) { registerPostTypeActions( postType ); }, [ registerPostTypeActions, postType ] ); - const duplicatePostAction = useDuplicatePostAction( postType ); - const isTemplateOrTemplatePart = [ - TEMPLATE_POST_TYPE, - TEMPLATE_PART_POST_TYPE, - ].includes( postType ); - const isPattern = postType === PATTERN_POST_TYPE; const isLoaded = !! postTypeObject; const supportsRevisions = !! postTypeObject?.supports?.revisions; return useMemo( () => { @@ -300,11 +93,6 @@ export function usePostActions( { postType, onActionPerformed, context } ) { let actions = [ postTypeObject?.viewable && viewPostAction, supportsRevisions && postRevisionsAction, - globalThis.IS_GUTENBERG_PLUGIN - ? ! isTemplateOrTemplatePart && - ! isPattern && - duplicatePostAction - : false, ...defaultActions, ].filter( Boolean ); // Filter actions based on provided context. If not provided @@ -370,10 +158,7 @@ export function usePostActions( { postType, onActionPerformed, context } ) { return actions; }, [ defaultActions, - isTemplateOrTemplatePart, - isPattern, postTypeObject?.viewable, - duplicatePostAction, onActionPerformed, isLoaded, supportsRevisions, diff --git a/packages/editor/src/dataviews/actions/duplicate-post.native.tsx b/packages/editor/src/dataviews/actions/duplicate-post.native.tsx new file mode 100644 index 00000000000000..5468aa649abbd4 --- /dev/null +++ b/packages/editor/src/dataviews/actions/duplicate-post.native.tsx @@ -0,0 +1,3 @@ +const duplicatePost = undefined; + +export default duplicatePost; diff --git a/packages/editor/src/dataviews/actions/duplicate-post.tsx b/packages/editor/src/dataviews/actions/duplicate-post.tsx new file mode 100644 index 00000000000000..0979d30da39519 --- /dev/null +++ b/packages/editor/src/dataviews/actions/duplicate-post.tsx @@ -0,0 +1,174 @@ +/** + * WordPress dependencies + */ +import { useDispatch } from '@wordpress/data'; +import { decodeEntities } from '@wordpress/html-entities'; +import { store as coreStore } from '@wordpress/core-data'; +import { __, sprintf, _x } from '@wordpress/i18n'; +import { store as noticesStore } from '@wordpress/notices'; +import { useState } from '@wordpress/element'; +import { DataForm } from '@wordpress/dataviews'; +import { + Button, + __experimentalHStack as HStack, + __experimentalVStack as VStack, +} from '@wordpress/components'; +import type { Action } from '@wordpress/dataviews'; + +/** + * Internal dependencies + */ +import { getItemTitle } from '../../dataviews/actions/utils'; +import type { CoreDataError, BasePost } from '../types'; +import { titleField } from '../fields'; + +const fields = [ titleField ]; +const formDuplicateAction = { + fields: [ 'title' ], +}; + +const duplicatePost: Action< BasePost > = { + id: 'duplicate-post', + label: _x( 'Duplicate', 'action label' ), + isEligible( { status } ) { + return status !== 'trash'; + }, + RenderModal: ( { items, closeModal, onActionPerformed } ) => { + const [ item, setItem ] = useState< BasePost >( { + ...items[ 0 ], + title: sprintf( + /* translators: %s: Existing template title */ + __( '%s (Copy)' ), + getItemTitle( items[ 0 ] ) + ), + } ); + + const [ isCreatingPage, setIsCreatingPage ] = useState( false ); + const { saveEntityRecord } = useDispatch( coreStore ); + const { createSuccessNotice, createErrorNotice } = + useDispatch( noticesStore ); + + async function createPage( event: React.FormEvent ) { + event.preventDefault(); + + if ( isCreatingPage ) { + return; + } + + const newItemOject = { + status: 'draft', + title: item.title, + slug: item.title || __( 'No title' ), + comment_status: item.comment_status, + content: + typeof item.content === 'string' + ? item.content + : item.content.raw, + excerpt: + typeof item.excerpt === 'string' + ? item.excerpt + : item.excerpt?.raw, + meta: item.meta, + parent: item.parent, + password: item.password, + template: item.template, + format: item.format, + featured_media: item.featured_media, + menu_order: item.menu_order, + ping_status: item.ping_status, + }; + const assignablePropertiesPrefix = 'wp:action-assign-'; + // Get all the properties that the current user is able to assign normally author, categories, tags, + // and custom taxonomies. + const assignableProperties = Object.keys( item?._links || {} ) + .filter( ( property ) => + property.startsWith( assignablePropertiesPrefix ) + ) + .map( ( property ) => + property.slice( assignablePropertiesPrefix.length ) + ); + assignableProperties.forEach( ( property ) => { + if ( item.hasOwnProperty( property ) ) { + // @ts-ignore + newItemOject[ property ] = item[ property ]; + } + } ); + setIsCreatingPage( true ); + try { + const newItem = await saveEntityRecord( + 'postType', + item.type, + newItemOject, + { throwOnError: true } + ); + + createSuccessNotice( + sprintf( + // translators: %s: Title of the created template e.g: "Category". + __( '"%s" successfully created.' ), + decodeEntities( newItem.title?.rendered || item.title ) + ), + { + id: 'duplicate-post-action', + type: 'snackbar', + } + ); + + if ( onActionPerformed ) { + onActionPerformed( [ newItem ] ); + } + } catch ( error ) { + const typedError = error as CoreDataError; + const errorMessage = + typedError.message && typedError.code !== 'unknown_error' + ? typedError.message + : __( 'An error occurred while duplicating the page.' ); + + createErrorNotice( errorMessage, { + type: 'snackbar', + } ); + } finally { + setIsCreatingPage( false ); + closeModal?.(); + } + } + + return ( +
+ + + setItem( ( prev ) => ( { + ...prev, + ...changes, + } ) ) + } + /> + + + + + +
+ ); + }, +}; + +export default duplicatePost; diff --git a/packages/editor/src/dataviews/actions/reorder-page.tsx b/packages/editor/src/dataviews/actions/reorder-page.tsx index 1b9524123e7c95..1820884d8d8c73 100644 --- a/packages/editor/src/dataviews/actions/reorder-page.tsx +++ b/packages/editor/src/dataviews/actions/reorder-page.tsx @@ -17,7 +17,7 @@ import type { Action, RenderModalProps } from '@wordpress/dataviews'; /** * Internal dependencies */ -import type { CoreDataError, PostWithPageAttributesSupport } from '../types'; +import type { CoreDataError, BasePost } from '../types'; import { orderField } from '../fields'; const fields = [ orderField ]; @@ -29,7 +29,7 @@ function ReorderModal( { items, closeModal, onActionPerformed, -}: RenderModalProps< PostWithPageAttributesSupport > ) { +}: RenderModalProps< BasePost > ) { const [ item, setItem ] = useState( items[ 0 ] ); const orderInput = item.menu_order; const { editEntityRecord, saveEditedEntityRecord } = @@ -113,7 +113,7 @@ function ReorderModal( { ); } -const reorderPage: Action< PostWithPageAttributesSupport > = { +const reorderPage: Action< BasePost > = { id: 'order-pages', label: __( 'Order' ), isEligible( { status } ) { diff --git a/packages/editor/src/dataviews/fields/index.ts b/packages/editor/src/dataviews/fields/index.ts index ea30d15dab600b..b215172eaf7f02 100644 --- a/packages/editor/src/dataviews/fields/index.ts +++ b/packages/editor/src/dataviews/fields/index.ts @@ -7,17 +7,18 @@ import type { Field } from '@wordpress/dataviews'; /** * Internal dependencies */ -import type { BasePost, PostWithPageAttributesSupport } from '../types'; +import type { BasePost } from '../types'; +import { getItemTitle } from '../actions/utils'; export const titleField: Field< BasePost > = { type: 'text', id: 'title', label: __( 'Title' ), placeholder: __( 'No title' ), - getValue: ( { item } ) => item.title, + getValue: ( { item } ) => getItemTitle( item ), }; -export const orderField: Field< PostWithPageAttributesSupport > = { +export const orderField: Field< BasePost > = { type: 'integer', id: 'menu_order', label: __( 'Order' ), diff --git a/packages/editor/src/dataviews/store/private-actions.ts b/packages/editor/src/dataviews/store/private-actions.ts index 80449d1b7a0798..d5b299b012e364 100644 --- a/packages/editor/src/dataviews/store/private-actions.ts +++ b/packages/editor/src/dataviews/store/private-actions.ts @@ -21,6 +21,7 @@ import restorePost from '../actions/restore-post'; import type { PostType } from '../types'; import { store as editorStore } from '../../store'; import { unlock } from '../../lock-unlock'; +import duplicatePost from '../actions/duplicate-post'; export function registerEntityAction< Item >( kind: string, @@ -87,6 +88,14 @@ export const registerPostTypeActions = .getCurrentTheme(); const actions = [ + // @ts-ignore + globalThis.IS_GUTENBERG_PLUGIN + ? ! [ 'wp_template', 'wp_block', 'wp_template_part' ].includes( + postTypeConfig.slug + ) && + canCreate && + duplicatePost + : undefined, postTypeConfig.slug === 'wp_template_part' && canCreate && currentTheme?.is_block_theme && diff --git a/packages/editor/src/dataviews/types.ts b/packages/editor/src/dataviews/types.ts index 5750ab96eeae81..514953d6691290 100644 --- a/packages/editor/src/dataviews/types.ts +++ b/packages/editor/src/dataviews/types.ts @@ -7,7 +7,7 @@ type PostStatus = | 'auto-draft' | 'trash'; -export interface BasePost { +export interface CommonPost { status?: PostStatus; title: string | { rendered: string } | { raw: string }; content: string | { raw: string; rendered: string }; @@ -16,7 +16,21 @@ export interface BasePost { blocks?: Object[]; } -export interface Template extends BasePost { +export interface BasePost extends CommonPost { + comment_status?: 'open' | 'closed'; + excerpt?: string | { raw: string; rendered: string }; + meta?: Record< string, any >; + parent?: number; + password?: string; + template?: string; + format?: string; + featured_media?: number; + menu_order?: number; + ping_status?: 'open' | 'closed'; + _links?: Record< string, { href: string }[] >; +} + +export interface Template extends CommonPost { type: 'wp_template'; is_custom: boolean; source: string; @@ -24,7 +38,7 @@ export interface Template extends BasePost { id: string; } -export interface TemplatePart extends BasePost { +export interface TemplatePart extends CommonPost { type: 'wp_template_part'; source: string; has_theme_file: boolean; @@ -32,16 +46,12 @@ export interface TemplatePart extends BasePost { area: string; } -export interface Pattern extends BasePost { +export interface Pattern extends CommonPost { slug: string; title: { raw: string }; wp_pattern_sync_status: string; } -export interface PostWithPageAttributesSupport extends BasePost { - menu_order: number; -} - export type Post = Template | TemplatePart | Pattern | BasePost; export type PostWithPermissions = Post & { From f9268debccb0fb53bb7552dd5723cece5c1178a2 Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Tue, 13 Aug 2024 11:00:12 +0200 Subject: [PATCH 057/126] `Composite`: export `useCompositeStore`, add more focus-related props (#64450) * Export useCompositeStore from package * Add `accessibleWhenDisabled` prop to `Composite.Item` * Add `focusable`, `disabled`, `accessibleWhenDisabled`, and `onFocusVisible` props to `Composite` * CHANGELOG --- Co-authored-by: ciampo Co-authored-by: tyxla --- packages/components/CHANGELOG.md | 1 + packages/components/src/composite/README.md | 67 +++++++++++++++++ packages/components/src/composite/index.tsx | 6 +- .../src/composite/stories/index.story.tsx | 71 ++++++++++++++++++- packages/components/src/composite/types.ts | 67 +++++++++++++++++ packages/components/src/index.ts | 2 +- 6 files changed, 210 insertions(+), 4 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 57ba69197cfa98..761e6604a127a2 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -6,6 +6,7 @@ - `Composite`: add stable version of the component ([#63564](https://github.com/WordPress/gutenberg/pull/63564)). - `Composite`: add `Hover` and `Typeahead` subcomponents ([#64399](https://github.com/WordPress/gutenberg/pull/64399)). +- `Composite`: export `useCompositeStore, add focus-related props to `Composite`and`Composite.Item` subcomponents ([#64450](https://github.com/WordPress/gutenberg/pull/64450)). ### Enhancements diff --git a/packages/components/src/composite/README.md b/packages/components/src/composite/README.md index 7bd12d0cabfa0c..3670e31b01e9df 100644 --- a/packages/components/src/composite/README.md +++ b/packages/components/src/composite/README.md @@ -147,6 +147,55 @@ Allows the component to be rendered as a different HTML element or React compone - Required: no +##### `focusable`: `boolean` + +Makes the component a focusable element. When this element gains keyboard focus, it gets a `data-focus-visible` attribute and triggers the `onFocusVisible` prop. + +The component supports the `disabled` prop even for those elements not supporting the native `disabled` attribute. Disabled elements may be still accessible via keyboard by using the the `accessibleWhenDisabled` prop. + +Non-native focusable elements will lose their focusability entirely. However, native focusable elements will retain their inherent focusability. + +- Required: no + +##### `disabled`: `boolean` + +Determines if the element is disabled. This sets the `aria-disabled` attribute accordingly, enabling support for all elements, including those that don't support the native `disabled` attribute. + +This feature can be combined with the `accessibleWhenDisabled` prop to +make disabled elements still accessible via keyboard. + +**Note**: For this prop to work, the `focusable` prop must be set to +`true`, if it's not set by default. + +- Required: no +- Default: `false` + +##### `accessibleWhenDisabled`: `boolean` + +Indicates whether the element should be focusable even when it is +`disabled`. + +This is important when discoverability is a concern. For example: + +> A toolbar in an editor contains a set of special smart paste functions +> that are disabled when the clipboard is empty or when the function is not +> applicable to the current content of the clipboard. It could be helpful to +> keep the disabled buttons focusable if the ability to discover their +> functionality is primarily via their presence on the toolbar. + +Learn more on [Focusability of disabled +controls](https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/#focusabilityofdisabledcontrols). + +- Required: no + +##### `onFocusVisible`: `(event: SyntheticEvent) => void` + +Custom event handler invoked when the element gains focus through keyboard interaction or a key press occurs while the element is in focus. This is the programmatic equivalent of the `data-focus-visible` attribute. + +**Note**: For this prop to work, the `focusable` prop must be set to `true` if it's not set by default. + +- Required: no + ##### `children`: `React.ReactNode` The contents of the component. @@ -189,6 +238,24 @@ The contents of the component. Renders a composite item. +##### `accessibleWhenDisabled`: `boolean` + +Indicates whether the element should be focusable even when it is +`disabled`. + +This is important when discoverability is a concern. For example: + +> A toolbar in an editor contains a set of special smart paste functions +> that are disabled when the clipboard is empty or when the function is not +> applicable to the current content of the clipboard. It could be helpful to +> keep the disabled buttons focusable if the ability to discover their +> functionality is primarily via their presence on the toolbar. + +Learn more on [Focusability of disabled +controls](https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/#focusabilityofdisabledcontrols). + +- Required: no + ##### `render`: `RenderProp & { ref?: React.Ref | undefined; }> | React.ReactElement>` Allows the component to be rendered as a different HTML element or React component. The value can be a React element or a function that takes in the original component props and gives back a React element with the props merged. diff --git a/packages/components/src/composite/index.tsx b/packages/components/src/composite/index.tsx index 4e87b9a55fa5bb..f5d92330cada3c 100644 --- a/packages/components/src/composite/index.tsx +++ b/packages/components/src/composite/index.tsx @@ -136,8 +136,10 @@ export const Composite = Object.assign( forwardRef< HTMLDivElement, WordPressComponentProps< CompositeProps, 'div', false > - >( function CompositeRow( props, ref ) { - return ; + >( function Composite( { disabled = false, ...props }, ref ) { + return ( + + ); } ), { displayName: 'Composite', diff --git a/packages/components/src/composite/stories/index.story.tsx b/packages/components/src/composite/stories/index.story.tsx index f1be53445f79ad..405962b92a761c 100644 --- a/packages/components/src/composite/stories/index.story.tsx +++ b/packages/components/src/composite/stories/index.story.tsx @@ -74,6 +74,28 @@ const meta: Meta< typeof UseCompositeStorePlaceholder > = { table: { type: { summary: 'React.ReactNode' } }, }, }; + const accessibleWhenDisabled = { + name: 'accessibleWhenDisabled', + description: `Indicates whether the element should be focusable even when it is +\`disabled\`. + +This is important when discoverability is a concern. For example: + +> A toolbar in an editor contains a set of special smart paste functions +> that are disabled when the clipboard is empty or when the function is not +> applicable to the current content of the clipboard. It could be helpful to +> keep the disabled buttons focusable if the ability to discover their +> functionality is primarily via their presence on the toolbar. + +Learn more on [Focusability of disabled +controls](https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/#focusabilityofdisabledcontrols).`, + table: { + type: { + summary: 'boolean', + }, + }, + }; + const argTypes = { useCompositeStore: { activeId: { @@ -226,11 +248,58 @@ This only affects the composite widget behavior. You still need to set \`dir="rt }, type: { required: true }, }, + focusable: { + name: 'focusable', + description: `Makes the component a focusable element. When this element gains keyboard focus, it gets a \`data-focus-visible\` attribute and triggers the \`onFocusVisible\` prop. + +The component supports the \`disabled\` prop even for those elements not supporting the native \`disabled\` attribute. Disabled elements may be still accessible via keyboard by using the the \`accessibleWhenDisabled\` prop. + +Non-native focusable elements will lose their focusability entirely. However, native focusable elements will retain their inherent focusability.`, + table: { + type: { + summary: 'boolean', + }, + }, + }, + disabled: { + name: 'disabled', + description: `Determines if the element is disabled. This sets the \`aria-disabled\` attribute accordingly, enabling support for all elements, including those that don't support the native \`disabled\` attribute. + +This feature can be combined with the \`accessibleWhenDisabled\` prop to +make disabled elements still accessible via keyboard. + +**Note**: For this prop to work, the \`focusable\` prop must be set to +\`true\`, if it's not set by default.`, + table: { + defaultValue: { + summary: 'false', + }, + type: { + summary: 'boolean', + }, + }, + }, + accessibleWhenDisabled, + onFocusVisible: { + name: 'onFocusVisible', + description: `Custom event handler invoked when the element gains focus through keyboard interaction or a key press occurs while the element is in focus. This is the programmatic equivalent of the \`data-focus-visible\` attribute. + +**Note**: For this prop to work, the \`focusable\` prop must be set to \`true\` if it's not set by default.`, + table: { + type: { + summary: + '(event: SyntheticEvent) => void', + }, + }, + }, }, 'Composite.Group': commonArgTypes, 'Composite.GroupLabel': commonArgTypes, 'Composite.Row': commonArgTypes, - 'Composite.Item': commonArgTypes, + 'Composite.Item': { + ...commonArgTypes, + accessibleWhenDisabled, + }, 'Composite.Hover': commonArgTypes, 'Composite.Typeahead': commonArgTypes, }; diff --git a/packages/components/src/composite/types.ts b/packages/components/src/composite/types.ts index 8bd4b447a83aef..5afe410f7582ba 100644 --- a/packages/components/src/composite/types.ts +++ b/packages/components/src/composite/types.ts @@ -131,6 +131,57 @@ export type CompositeProps = { * merged. */ render?: Ariakit.CompositeProps[ 'render' ]; + /** + * Makes the component a focusable element. When this element gains keyboard + * focus, it gets a `data-focus-visible` attribute and triggers the + * `onFocusVisible` prop. + * The component supports the `disabled` prop even for those elements not + * supporting the native `disabled` attribute. Disabled elements may be + * still accessible via keyboard by using the the `accessibleWhenDisabled` + * prop. + * Non-native focusable elements will lose their focusability entirely. + * However, native focusable elements will retain their inherent focusability. + */ + focusable?: Ariakit.CompositeProps[ 'focusable' ]; + /** + * Determines if the element is disabled. This sets the `aria-disabled` + * attribute accordingly, enabling support for all elements, including those + * that don't support the native `disabled` attribute. + * + * This feature can be combined with the `accessibleWhenDisabled` prop to + * make disabled elements still accessible via keyboard. + * + * **Note**: For this prop to work, the `focusable` prop must be set to + * `true`, if it's not set by default. + * + * @default false + */ + disabled?: Ariakit.CompositeProps[ 'disabled' ]; + /** + * Indicates whether the element should be focusable even when it is + * `disabled`. + * + * This is important when discoverability is a concern. For example: + * + * > A toolbar in an editor contains a set of special smart paste functions + * that are disabled when the clipboard is empty or when the function is not + * applicable to the current content of the clipboard. It could be helpful to + * keep the disabled buttons focusable if the ability to discover their + * functionality is primarily via their presence on the toolbar. + * + * Learn more on [Focusability of disabled + * controls](https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/#focusabilityofdisabledcontrols). + */ + accessibleWhenDisabled?: Ariakit.CompositeProps[ 'accessibleWhenDisabled' ]; + /** + * Custom event handler invoked when the element gains focus through keyboard + * interaction or a key press occurs while the element is in focus. This is + * the programmatic equivalent of the `data-focus-visible` attribute. + * + * **Note**: For this prop to work, the `focusable` prop must be set to `true` + * if it's not set by default. + */ + onFocusVisible?: Ariakit.CompositeProps[ 'onFocusVisible' ]; /** * The contents of the component. */ @@ -177,6 +228,22 @@ export type CompositeItemProps = { * The contents of the component. */ children?: Ariakit.CompositeItemProps[ 'children' ]; + /** + * Indicates whether the element should be focusable even when it is + * `disabled`. + * + * This is important when discoverability is a concern. For example: + * + * > A toolbar in an editor contains a set of special smart paste functions + * that are disabled when the clipboard is empty or when the function is not + * applicable to the current content of the clipboard. It could be helpful to + * keep the disabled buttons focusable if the ability to discover their + * functionality is primarily via their presence on the toolbar. + * + * Learn more on [Focusability of disabled + * controls](https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/#focusabilityofdisabledcontrols). + */ + accessibleWhenDisabled?: Ariakit.CompositeItemProps[ 'accessibleWhenDisabled' ]; }; export type CompositeRowProps = { diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index 4c724a461e6775..cd6d2a77db9cb6 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -62,7 +62,7 @@ export { CompositeItem as __unstableCompositeItem, useCompositeState as __unstableUseCompositeState, } from './composite/legacy'; -export { Composite } from './composite'; +export { Composite, useCompositeStore } from './composite'; export { ConfirmDialog as __experimentalConfirmDialog } from './confirm-dialog'; export { default as CustomSelectControl } from './custom-select-control'; export { default as Dashicon } from './dashicon'; From ad37f2063021a9d151253ca1386c213ef300fe6d Mon Sep 17 00:00:00 2001 From: Ben Dwyer Date: Tue, 13 Aug 2024 10:03:02 +0100 Subject: [PATCH 058/126] Zoom Out: Defalt the inserter in the patterns tab when in zoom out (#64193) Co-authored-by: scruffian Co-authored-by: MaggieCabrera --- .../block-editor/src/components/inserter/menu.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/block-editor/src/components/inserter/menu.js b/packages/block-editor/src/components/inserter/menu.js index c5f41f9e3bf0a0..629765315c1d6b 100644 --- a/packages/block-editor/src/components/inserter/menu.js +++ b/packages/block-editor/src/components/inserter/menu.js @@ -73,9 +73,16 @@ function InserterMenu( const [ patternFilter, setPatternFilter ] = useState( 'all' ); const [ selectedMediaCategory, setSelectedMediaCategory ] = useState( null ); - const [ selectedTab, setSelectedTab ] = useState( - __experimentalInitialTab - ); + function getInitialTab() { + if ( __experimentalInitialTab ) { + return __experimentalInitialTab; + } + + if ( isZoomOutMode ) { + return 'patterns'; + } + } + const [ selectedTab, setSelectedTab ] = useState( getInitialTab() ); const [ destinationRootClientId, onInsertBlocks, onToggleInsertionPoint ] = useInsertionPoint( { From 943b72aecdfa160e925e43dad4cfbc9f9b277810 Mon Sep 17 00:00:00 2001 From: Hit Bhalodia <58802366+hbhalodia@users.noreply.github.com> Date: Tue, 13 Aug 2024 14:39:56 +0530 Subject: [PATCH 059/126] Fix: Embed blocks: adding captions via toolbar (#64394) * Add the Caption component to be used for consistency on the embed block similar to audio and image * Change the label for the embed caption text Co-authored-by: hbhalodia Co-authored-by: Rishit30G Co-authored-by: t-hamano Co-authored-by: simison --- packages/block-library/src/embed/edit.js | 9 +++++ .../block-library/src/embed/embed-preview.js | 40 ++----------------- 2 files changed, 12 insertions(+), 37 deletions(-) diff --git a/packages/block-library/src/embed/edit.js b/packages/block-library/src/embed/edit.js index 12265883d6963c..0b589eb68ecf49 100644 --- a/packages/block-library/src/embed/edit.js +++ b/packages/block-library/src/embed/edit.js @@ -14,6 +14,7 @@ import { embedContentIcon } from './icons'; import EmbedLoading from './embed-loading'; import EmbedPlaceholder from './embed-placeholder'; import EmbedPreview from './embed-preview'; +import { Caption } from '../utils/caption'; /** * External dependencies @@ -277,6 +278,14 @@ const EmbedEdit = ( props ) => { label={ label } insertBlocksAfter={ insertBlocksAfter } /> + ); diff --git a/packages/block-library/src/embed/embed-preview.js b/packages/block-library/src/embed/embed-preview.js index ba079cc2e1e8b6..d53f1148cee13c 100644 --- a/packages/block-library/src/embed/embed-preview.js +++ b/packages/block-library/src/embed/embed-preview.js @@ -13,13 +13,8 @@ import clsx from 'clsx'; */ import { __, sprintf } from '@wordpress/i18n'; import { Placeholder, SandBox } from '@wordpress/components'; -import { - RichText, - BlockIcon, - __experimentalGetElementClassName, -} from '@wordpress/block-editor'; +import { BlockIcon } from '@wordpress/block-editor'; import { Component } from '@wordpress/element'; -import { createBlock, getDefaultBlockName } from '@wordpress/blocks'; import { getAuthority } from '@wordpress/url'; /** @@ -57,19 +52,8 @@ class EmbedPreview extends Component { } render() { - const { - preview, - previewable, - url, - type, - caption, - onCaptionChange, - isSelected, - className, - icon, - label, - insertBlocksAfter, - } = this.props; + const { preview, previewable, url, type, className, icon, label } = + this.props; const { scripts } = preview; const { interactive } = this.state; @@ -139,24 +123,6 @@ class EmbedPreview extends Component {

) } - { ( ! RichText.isEmpty( caption ) || isSelected ) && ( - - insertBlocksAfter( - createBlock( getDefaultBlockName() ) - ) - } - /> - ) } ); } From 600aa54ca87ad53b8b7e81977651ba0e5fc9affc Mon Sep 17 00:00:00 2001 From: Aki Hamano <54422211+t-hamano@users.noreply.github.com> Date: Tue, 13 Aug 2024 18:27:00 +0900 Subject: [PATCH 060/126] Table Block: Hide caption toolbar button on multiple selection (#64462) Co-authored-by: t-hamano Co-authored-by: Mamaduka --- packages/block-library/src/table/edit.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/block-library/src/table/edit.js b/packages/block-library/src/table/edit.js index 5510d0031ff6be..95162f4e14c00b 100644 --- a/packages/block-library/src/table/edit.js +++ b/packages/block-library/src/table/edit.js @@ -95,7 +95,7 @@ function TableEdit( { attributes, setAttributes, insertBlocksAfter, - isSelected, + isSelected: isSingleSelected, } ) { const { hasFixedLayout, head, foot } = attributes; const [ initialRowCount, setInitialRowCount ] = useState( 2 ); @@ -340,10 +340,10 @@ function TableEdit( { } useEffect( () => { - if ( ! isSelected ) { + if ( ! isSingleSelected ) { setSelectedCell(); } - }, [ isSelected ] ); + }, [ isSingleSelected ] ); useEffect( () => { if ( hasTableCreated ) { @@ -565,9 +565,10 @@ function TableEdit( { ) } From 992bcde7651e93d3115f6c54e8bb485acd808bc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Albert=20Juh=C3=A9=20Lluveras?= Date: Tue, 13 Aug 2024 11:57:24 +0200 Subject: [PATCH 061/126] Add plugin template registration API (#61577) Co-authored-by: Aljullu Co-authored-by: cbravobernal Co-authored-by: ntsekouras Co-authored-by: peterwilsoncc Co-authored-by: talldan Co-authored-by: youknowriad Co-authored-by: mtias Co-authored-by: gziolo Co-authored-by: SantosGuillamot Co-authored-by: TimothyBJacobs Co-authored-by: ellatrix Co-authored-by: ndiego Co-authored-by: carolinan Co-authored-by: nerrad Co-authored-by: annezazu --- backport-changelog/6.7/7125.md | 3 + lib/compat/wordpress-6.7/block-templates.php | 41 ++ ...utenberg-rest-templates-controller-6-7.php | 203 ++++++++++ .../class-wp-block-templates-registry.php | 256 +++++++++++++ lib/compat/wordpress-6.7/compat.php | 114 ++++++ lib/compat/wordpress-6.7/rest-api.php | 51 +++ lib/load.php | 4 + .../core-data/src/entity-types/wp-template.ts | 4 + .../plugins/block-template-registration.php | 72 ++++ .../src/utils/is-template-removable.js | 6 +- .../src/utils/is-template-revertable.js | 3 +- .../src/dataviews/actions/reset-post.tsx | 3 +- .../editor/src/dataviews/actions/utils.ts | 4 +- packages/editor/src/dataviews/types.ts | 3 + packages/editor/src/store/private-actions.js | 2 +- .../src/store/utils/is-template-revertable.js | 3 +- phpunit/block-template-test.php | 37 ++ ...tenberg-rest-templates-controller-test.php | 119 ++++++ ...class-wp-block-templates-registry-test.php | 192 ++++++++++ .../site-editor/template-registration.spec.js | 356 ++++++++++++++++++ 20 files changed, 1469 insertions(+), 7 deletions(-) create mode 100644 backport-changelog/6.7/7125.md create mode 100644 lib/compat/wordpress-6.7/block-templates.php create mode 100644 lib/compat/wordpress-6.7/class-gutenberg-rest-templates-controller-6-7.php create mode 100644 lib/compat/wordpress-6.7/class-wp-block-templates-registry.php create mode 100644 lib/compat/wordpress-6.7/compat.php create mode 100644 packages/e2e-tests/plugins/block-template-registration.php create mode 100644 phpunit/block-template-test.php create mode 100644 phpunit/class-gutenberg-rest-templates-controller-test.php create mode 100644 phpunit/class-wp-block-templates-registry-test.php create mode 100644 test/e2e/specs/site-editor/template-registration.spec.js diff --git a/backport-changelog/6.7/7125.md b/backport-changelog/6.7/7125.md new file mode 100644 index 00000000000000..ce208decd2d145 --- /dev/null +++ b/backport-changelog/6.7/7125.md @@ -0,0 +1,3 @@ +https://github.com/WordPress/wordpress-develop/pull/7125 + +* https://github.com/WordPress/gutenberg/pull/61577 diff --git a/lib/compat/wordpress-6.7/block-templates.php b/lib/compat/wordpress-6.7/block-templates.php new file mode 100644 index 00000000000000..e270ab226c1d9f --- /dev/null +++ b/lib/compat/wordpress-6.7/block-templates.php @@ -0,0 +1,41 @@ +register( $template_name, $args ); + } +} + +if ( ! function_exists( 'wp_unregister_block_template' ) ) { + /** + * Unregister a template. + * + * @param string $template_name Template name in the form of `plugin_uri//template_name`. + * @return true|WP_Error True on success, WP_Error on failure or if the template doesn't exist. + */ + function wp_unregister_block_template( $template_name ) { + return WP_Block_Templates_Registry::get_instance()->unregister( $template_name ); + } +} diff --git a/lib/compat/wordpress-6.7/class-gutenberg-rest-templates-controller-6-7.php b/lib/compat/wordpress-6.7/class-gutenberg-rest-templates-controller-6-7.php new file mode 100644 index 00000000000000..ed67dded75ecb1 --- /dev/null +++ b/lib/compat/wordpress-6.7/class-gutenberg-rest-templates-controller-6-7.php @@ -0,0 +1,203 @@ +post_type ); + } else { + $template = get_block_template( $request['id'], $this->post_type ); + } + + if ( ! $template ) { + return new WP_Error( 'rest_template_not_found', __( 'No templates exist with that id.' ), array( 'status' => 404 ) ); + } + + return $this->prepare_item_for_response( $template, $request ); + } + + /** + * Prepare a single template output for response + * + * @param WP_Block_Template $item Template instance. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response Response object. + */ + // @core-merge: Fix wrong author in plugin templates. + public function prepare_item_for_response( $item, $request ) { + $template = $item; + + $fields = $this->get_fields_for_response( $request ); + + if ( 'plugin' !== $item->origin ) { + return parent::prepare_item_for_response( $item, $request ); + } + $cloned_item = clone $item; + // Set the origin as theme when calling the previous `prepare_item_for_response()` to prevent warnings when generating the author text. + $cloned_item->origin = 'theme'; + $response = parent::prepare_item_for_response( $cloned_item, $request ); + $data = $response->data; + + if ( rest_is_field_included( 'origin', $fields ) ) { + $data['origin'] = 'plugin'; + } + + if ( rest_is_field_included( 'plugin', $fields ) ) { + $registered_template = WP_Block_Templates_Registry::get_instance()->get_by_slug( $cloned_item->slug ); + if ( $registered_template ) { + $data['plugin'] = $registered_template->plugin; + } + } + + if ( rest_is_field_included( 'author_text', $fields ) ) { + $data['author_text'] = $this->get_wp_templates_author_text_field( $template ); + } + + if ( rest_is_field_included( 'original_source', $fields ) ) { + $data['original_source'] = $this->get_wp_templates_original_source_field( $template ); + } + + $response = rest_ensure_response( $data ); + + if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) { + $links = $this->prepare_links( $template->id ); + $response->add_links( $links ); + if ( ! empty( $links['self']['href'] ) ) { + $actions = $this->get_available_actions(); + $self = $links['self']['href']; + foreach ( $actions as $rel ) { + $response->add_link( $rel, $self ); + } + } + } + + return $response; + } + + /** + * Returns the source from where the template originally comes from. + * + * @param WP_Block_Template $template_object Template instance. + * @return string Original source of the template one of theme, plugin, site, or user. + */ + // @core-merge: Changed the comments format (from inline to multi-line) in the entire function. + private static function get_wp_templates_original_source_field( $template_object ) { + if ( 'wp_template' === $template_object->type || 'wp_template_part' === $template_object->type ) { + /* + * Added by theme. + * Template originally provided by a theme, but customized by a user. + * Templates originally didn't have the 'origin' field so identify + * older customized templates by checking for no origin and a 'theme' + * or 'custom' source. + */ + if ( $template_object->has_theme_file && + ( 'theme' === $template_object->origin || ( + empty( $template_object->origin ) && in_array( + $template_object->source, + array( + 'theme', + 'custom', + ), + true + ) ) + ) + ) { + return 'theme'; + } + + // Added by plugin. + // @core-merge: Removed `$template_object->has_theme_file` check from this if clause. + if ( 'plugin' === $template_object->origin ) { + return 'plugin'; + } + + /* + * Added by site. + * Template was created from scratch, but has no author. Author support + * was only added to templates in WordPress 5.9. Fallback to showing the + * site logo and title. + */ + if ( empty( $template_object->has_theme_file ) && 'custom' === $template_object->source && empty( $template_object->author ) ) { + return 'site'; + } + } + + // Added by user. + return 'user'; + } + + /** + * Returns a human readable text for the author of the template. + * + * @param WP_Block_Template $template_object Template instance. + * @return string Human readable text for the author. + */ + private static function get_wp_templates_author_text_field( $template_object ) { + $original_source = self::get_wp_templates_original_source_field( $template_object ); + switch ( $original_source ) { + case 'theme': + $theme_name = wp_get_theme( $template_object->theme )->get( 'Name' ); + return empty( $theme_name ) ? $template_object->theme : $theme_name; + case 'plugin': + // @core-merge: Prioritize plugin name instead of theme name for plugin-registered templates. + if ( ! function_exists( 'get_plugins' ) || ! function_exists( 'get_plugin_data' ) ) { + require_once ABSPATH . 'wp-admin/includes/plugin.php'; + } + if ( isset( $template_object->plugin ) ) { + $plugins = wp_get_active_and_valid_plugins(); + + foreach ( $plugins as $plugin_file ) { + $plugin_basename = plugin_basename( $plugin_file ); + // Split basename by '/' to get the plugin slug. + list( $plugin_slug, ) = explode( '/', $plugin_basename ); + + if ( $plugin_slug === $template_object->plugin ) { + $plugin_data = get_plugin_data( $plugin_file ); + + if ( ! empty( $plugin_data['Name'] ) ) { + return $plugin_data['Name']; + } + + break; + } + } + } + + /* + * Fall back to the theme name if the plugin is not defined. That's needed to keep backwards + * compatibility with templates that were registered before the plugin attribute was added. + */ + $plugins = get_plugins(); + $plugin_basename = plugin_basename( sanitize_text_field( $template_object->theme . '.php' ) ); + if ( isset( $plugins[ $plugin_basename ] ) && isset( $plugins[ $plugin_basename ]['Name'] ) ) { + return $plugins[ $plugin_basename ]['Name']; + } + return isset( $template_object->plugin ) ? + $template_object->plugin : + $template_object->theme; + // @core-merge: End of changes to merge in core. + case 'site': + return get_bloginfo( 'name' ); + case 'user': + $author = get_user_by( 'id', $template_object->author ); + if ( ! $author ) { + return __( 'Unknown author' ); + } + return $author->get( 'display_name' ); + } + } +} diff --git a/lib/compat/wordpress-6.7/class-wp-block-templates-registry.php b/lib/compat/wordpress-6.7/class-wp-block-templates-registry.php new file mode 100644 index 00000000000000..db53f735e13b3d --- /dev/null +++ b/lib/compat/wordpress-6.7/class-wp-block-templates-registry.php @@ -0,0 +1,256 @@ + $instance` pairs. + * + * @since 6.7.0 + * @var WP_Block_Template[] $registered_block_templates Registered templates. + */ + private $registered_templates = array(); + + /** + * Container for the main instance of the class. + * + * @since 6.7.0 + * @var WP_Block_Templates_Registry|null + */ + private static $instance = null; + + /** + * Registers a template. + * + * @since 6.7.0 + * + * @param string $template_name Template name including namespace. + * @param array $args Optional. Array of template arguments. + * @return WP_Block_Template|WP_Error The registered template on success, or false on failure. + */ + public function register( $template_name, $args = array() ) { + + $template = null; + + $error_message = ''; + $error_code = ''; + if ( ! is_string( $template_name ) ) { + $error_message = __( 'Template names must be strings.', 'gutenberg' ); + $error_code = 'template_name_no_string'; + } elseif ( preg_match( '/[A-Z]+/', $template_name ) ) { + $error_message = __( 'Template names must not contain uppercase characters.', 'gutenberg' ); + $error_code = 'template_name_no_uppercase'; + } elseif ( ! preg_match( '/^[a-z0-9-]+\/\/[a-z0-9-]+$/', $template_name ) ) { + $error_message = __( 'Template names must contain a namespace prefix. Example: my-plugin//my-custom-template', 'gutenberg' ); + $error_code = 'template_no_prefix'; + } elseif ( $this->is_registered( $template_name ) ) { + /* translators: %s: Template name. */ + $error_message = sprintf( __( 'Template "%s" is already registered.', 'gutenberg' ), $template_name ); + $error_code = 'template_already_registered'; + } + + if ( $error_message ) { + _doing_it_wrong( + __METHOD__, + $error_message, + '6.7.0' + ); + return new WP_Error( $error_code, $error_message ); + } + + if ( ! $template ) { + $theme_name = get_stylesheet(); + list( $plugin, $slug ) = explode( '//', $template_name ); + $default_template_types = get_default_block_template_types(); + + $template = new WP_Block_Template(); + $template->id = $theme_name . '//' . $slug; + $template->theme = $theme_name; + $template->plugin = $plugin; + $template->author = null; + $template->content = isset( $args['content'] ) ? $args['content'] : ''; + $template->source = 'plugin'; + $template->slug = $slug; + $template->type = 'wp_template'; + $template->title = isset( $args['title'] ) ? $args['title'] : $template_name; + $template->description = isset( $args['description'] ) ? $args['description'] : ''; + $template->status = 'publish'; + $template->origin = 'plugin'; + $template->is_custom = ! isset( $default_template_types[ $template_name ] ); + $template->post_types = isset( $args['post_types'] ) ? $args['post_types'] : array(); + } + + $this->registered_templates[ $template_name ] = $template; + + return $template; + } + + /** + * Retrieves all registered templates. + * + * @since 6.7.0 + * + * @return WP_Block_Template[]|false Associative array of `$template_name => $template` pairs. + */ + public function get_all_registered() { + return $this->registered_templates; + } + + /** + * Retrieves a registered template by its name. + * + * @since 6.7.0 + * + * @param string $template_name Template name including namespace. + * @return WP_Block_Template|null|false The registered template, or null if it is not registered. + */ + public function get_registered( $template_name ) { + if ( ! $this->is_registered( $template_name ) ) { + return null; + } + + return $this->registered_templates[ $template_name ]; + } + + /** + * Retrieves a registered template by its slug. + * + * @since 6.7.0 + * + * @param string $template_slug Slug of the template. + * @return WP_Block_Template|null The registered template, or null if it is not registered. + */ + public function get_by_slug( $template_slug ) { + $all_templates = $this->get_all_registered(); + + if ( ! $all_templates ) { + return null; + } + + foreach ( $all_templates as $template ) { + if ( $template->slug === $template_slug ) { + return $template; + } + } + + return null; + } + + /** + * Retrieves registered templates matching a query. + * + * @since 6.7.0 + * + * @param array $query { + * Arguments to retrieve templates. Optional, empty by default. + * + * @type string[] $slug__in List of slugs to include. + * @type string[] $slug__not_in List of slugs to skip. + * @type string $post_type Post type to get the templates for. + * } + */ + public function get_by_query( $query = array() ) { + $all_templates = $this->get_all_registered(); + + if ( ! $all_templates ) { + return array(); + } + + $query = wp_parse_args( + $query, + array( + 'slug__in' => array(), + 'slug__not_in' => array(), + 'post_type' => '', + ) + ); + $slugs_to_include = $query['slug__in']; + $slugs_to_skip = $query['slug__not_in']; + $post_type = $query['post_type']; + + $matching_templates = array(); + foreach ( $all_templates as $template_name => $template ) { + if ( $slugs_to_include && ! in_array( $template->slug, $slugs_to_include, true ) ) { + continue; + } + + if ( $slugs_to_skip && in_array( $template->slug, $slugs_to_skip, true ) ) { + continue; + } + + if ( $post_type && ! in_array( $post_type, $template->post_types, true ) ) { + continue; + } + + $matching_templates[ $template_name ] = $template; + } + + return $matching_templates; + } + + /** + * Checks if a template is registered. + * + * @since 6.7.0 + * + * @param string $template_name Template name. + * @return bool True if the template is registered, false otherwise. + */ + public function is_registered( $template_name ) { + return isset( $this->registered_templates[ $template_name ] ); + } + + /** + * Unregisters a template. + * + * @since 6.7.0 + * + * @param string $template_name Template name including namespace. + * @return WP_Block_Template|false The unregistered template on success, or false on failure. + */ + public function unregister( $template_name ) { + if ( ! $this->is_registered( $template_name ) ) { + _doing_it_wrong( + __METHOD__, + /* translators: %s: Template name. */ + sprintf( __( 'Template "%s" is not registered.', 'gutenberg' ), $template_name ), + '6.7.0' + ); + /* translators: %s: Template name. */ + return new WP_Error( 'template_not_registered', __( 'Template "%s" is not registered.', 'gutenberg' ) ); + } + + $unregistered_template = $this->registered_templates[ $template_name ]; + unset( $this->registered_templates[ $template_name ] ); + + return $unregistered_template; + } + + /** + * Utility method to retrieve the main instance of the class. + * + * The instance will be created if it does not exist yet. + * + * @since 6.7.0 + * + * @return WP_Block_Templates_Registry The main instance. + */ + public static function get_instance() { + if ( null === self::$instance ) { + self::$instance = new self(); + } + + return self::$instance; + } + } +} diff --git a/lib/compat/wordpress-6.7/compat.php b/lib/compat/wordpress-6.7/compat.php new file mode 100644 index 00000000000000..7021cab2053eff --- /dev/null +++ b/lib/compat/wordpress-6.7/compat.php @@ -0,0 +1,114 @@ + $value ) { + $registered_template = WP_Block_Templates_Registry::get_instance()->get_by_slug( $query_result[ $key ]->slug ); + if ( $registered_template ) { + $query_result[ $key ]->plugin = $registered_template->plugin; + $query_result[ $key ]->origin = + 'theme' !== $query_result[ $key ]->origin && 'theme' !== $query_result[ $key ]->source ? + 'plugin' : + $query_result[ $key ]->origin; + } + } + + if ( ! isset( $query['wp_id'] ) ) { + $template_files = _gutenberg_get_block_templates_files( $template_type, $query ); + + /* + * Add templates registered in the template registry. Filtering out the ones which have a theme file. + */ + $registered_templates = WP_Block_Templates_Registry::get_instance()->get_by_query( $query ); + $matching_registered_templates = array_filter( + $registered_templates, + function ( $registered_template ) use ( $template_files ) { + foreach ( $template_files as $template_file ) { + if ( $template_file['slug'] === $registered_template->slug ) { + return false; + } + } + return true; + } + ); + $query_result = array_merge( $query_result, $matching_registered_templates ); + } + + return $query_result; +} +add_filter( 'get_block_templates', '_gutenberg_add_block_templates_from_registry', 10, 3 ); + +/** + * Hooks into `get_block_template` to add the `plugin` property when necessary. + * + * @param [WP_Block_Template|null] $block_template The found block template, or null if there isn’t one. + * @return [WP_Block_Template|null] The block template that was already found with the plugin property defined if it was reigstered by a plugin. + */ +function _gutenberg_add_block_template_plugin_attribute( $block_template ) { + if ( $block_template ) { + $registered_template = WP_Block_Templates_Registry::get_instance()->get_by_slug( $block_template->slug ); + if ( $registered_template ) { + $block_template->plugin = $registered_template->plugin; + $block_template->origin = + 'theme' !== $block_template->origin && 'theme' !== $block_template->source ? + 'plugin' : + $block_template->origin; + } + } + + return $block_template; +} +add_filter( 'get_block_template', '_gutenberg_add_block_template_plugin_attribute', 10, 1 ); + +/** + * Hooks into `get_block_file_template` so templates from the registry are also returned. + * + * @param WP_Block_Template|null $block_template The found block template, or null if there is none. + * @param string $id Template unique identifier (example: 'theme_slug//template_slug'). + * @return WP_Block_Template|null The block template that was already found or from the registry. In case the template was already found, add the necessary details from the registry. + */ +function _gutenberg_add_block_file_templates_from_registry( $block_template, $id ) { + if ( $block_template ) { + $registered_template = WP_Block_Templates_Registry::get_instance()->get_by_slug( $block_template->slug ); + if ( $registered_template ) { + $block_template->plugin = $registered_template->plugin; + $block_template->origin = + 'theme' !== $block_template->origin && 'theme' !== $block_template->source ? + 'plugin' : + $block_template->origin; + } + return $block_template; + } + + $parts = explode( '//', $id, 2 ); + + if ( count( $parts ) < 2 ) { + return $block_template; + } + + list( , $slug ) = $parts; + return WP_Block_Templates_Registry::get_instance()->get_by_slug( $slug ); +} +add_filter( 'get_block_file_template', '_gutenberg_add_block_file_templates_from_registry', 10, 2 ); diff --git a/lib/compat/wordpress-6.7/rest-api.php b/lib/compat/wordpress-6.7/rest-api.php index 713d31c4632c74..fe2aac9c2580ae 100644 --- a/lib/compat/wordpress-6.7/rest-api.php +++ b/lib/compat/wordpress-6.7/rest-api.php @@ -29,3 +29,54 @@ function gutenberg_block_editor_preload_paths_6_7( $paths, $context ) { return $paths; } add_filter( 'block_editor_rest_api_preload_paths', 'gutenberg_block_editor_preload_paths_6_7', 10, 2 ); + +if ( ! function_exists( 'wp_api_template_registry' ) ) { + /** + * Hook in to the template and template part post types and modify the rest + * endpoint to include modifications to read templates from the + * BlockTemplatesRegistry. + * + * @param array $args Current registered post type args. + * @param string $post_type Name of post type. + * + * @return array + */ + function wp_api_template_registry( $args, $post_type ) { + if ( 'wp_template' === $post_type || 'wp_template_part' === $post_type ) { + $args['rest_controller_class'] = 'Gutenberg_REST_Templates_Controller_6_7'; + } + return $args; + } +} +add_filter( 'register_post_type_args', 'wp_api_template_registry', 10, 2 ); + +/** + * Adds `plugin` fields to WP_REST_Templates_Controller class. + */ +function gutenberg_register_wp_rest_templates_controller_plugin_field() { + + register_rest_field( + 'wp_template', + 'plugin', + array( + 'get_callback' => function ( $template_object ) { + if ( $template_object ) { + $registered_template = WP_Block_Templates_Registry::get_instance()->get_by_slug( $template_object['slug'] ); + if ( $registered_template ) { + return $registered_template->plugin; + } + } + + return; + }, + 'update_callback' => null, + 'schema' => array( + 'type' => 'string', + 'description' => __( 'Plugin that registered the template.', 'gutenberg' ), + 'readonly' => true, + 'context' => array( 'view', 'edit', 'embed' ), + ), + ) + ); +} +add_action( 'rest_api_init', 'gutenberg_register_wp_rest_templates_controller_plugin_field' ); diff --git a/lib/load.php b/lib/load.php index c5f12af1654df2..b501f0abd1c978 100644 --- a/lib/load.php +++ b/lib/load.php @@ -41,6 +41,7 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/compat/wordpress-6.6/rest-api.php'; // WordPress 6.7 compat. + require __DIR__ . '/compat/wordpress-6.7/class-gutenberg-rest-templates-controller-6-7.php'; require __DIR__ . '/compat/wordpress-6.7/rest-api.php'; // Plugin specific code. @@ -101,9 +102,12 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/compat/wordpress-6.6/post.php'; // WordPress 6.7 compat. +require __DIR__ . '/compat/wordpress-6.7/block-templates.php'; require __DIR__ . '/compat/wordpress-6.7/blocks.php'; require __DIR__ . '/compat/wordpress-6.7/block-bindings.php'; require __DIR__ . '/compat/wordpress-6.7/script-modules.php'; +require __DIR__ . '/compat/wordpress-6.7/class-wp-block-templates-registry.php'; +require __DIR__ . '/compat/wordpress-6.7/compat.php'; // Experimental features. require __DIR__ . '/experimental/block-editor-settings-mobile.php'; diff --git a/packages/core-data/src/entity-types/wp-template.ts b/packages/core-data/src/entity-types/wp-template.ts index ac6db09035f193..70d3e40c295dcf 100644 --- a/packages/core-data/src/entity-types/wp-template.ts +++ b/packages/core-data/src/entity-types/wp-template.ts @@ -73,6 +73,10 @@ declare module './base-entity-records' { * Post ID. */ wp_id: number; + /** + * Plugin that registered the template. + */ + plugin?: string; /** * Theme file exists. */ diff --git a/packages/e2e-tests/plugins/block-template-registration.php b/packages/e2e-tests/plugins/block-template-registration.php new file mode 100644 index 00000000000000..a7c75552849658 --- /dev/null +++ b/packages/e2e-tests/plugins/block-template-registration.php @@ -0,0 +1,72 @@ + 'Plugin Template', + 'description' => 'A template registered by a plugin.', + 'content' => '

This is a plugin-registered template.

', + 'post_types' => array( 'post' ), + ) + ); + add_action( + 'category_template_hierarchy', + function () { + return array( 'plugin-template' ); + } + ); + + // Custom template overridden by the theme. + wp_register_block_template( + 'gutenberg//custom-template', + array( + 'title' => 'Custom Template (overridden by the theme)', + 'description' => 'A custom template registered by a plugin and overridden by a theme.', + 'content' => '

This is a plugin-registered template and overridden by a theme.

', + 'post_types' => array( 'post' ), + ) + ); + + // Custom template used to test unregistration. + wp_register_block_template( + 'gutenberg//plugin-unregistered-template', + array( + 'title' => 'Plugin Unregistered Template', + 'description' => 'A plugin-registered template that is unregistered.', + 'content' => '

This is a plugin-registered template that is also unregistered.

', + ) + ); + wp_unregister_block_template( 'gutenberg//plugin-unregistered-template' ); + + // Custom template used to test overriding default WP templates. + wp_register_block_template( + 'gutenberg//page', + array( + 'title' => 'Plugin Page Template', + 'description' => 'A plugin-registered page template.', + 'content' => '

This is a plugin-registered page template.

', + ) + ); + + // Custom template used to test overriding default WP templates which can be created by the user. + wp_register_block_template( + 'gutenberg//author-admin', + array( + 'title' => 'Plugin Author Template', + 'description' => 'A plugin-registered author template.', + 'content' => '

This is a plugin-registered author template.

', + ) + ); + } +); diff --git a/packages/edit-site/src/utils/is-template-removable.js b/packages/edit-site/src/utils/is-template-removable.js index 9cb1de23daab75..f81cb74b022e73 100644 --- a/packages/edit-site/src/utils/is-template-removable.js +++ b/packages/edit-site/src/utils/is-template-removable.js @@ -7,7 +7,7 @@ import { TEMPLATE_ORIGINS } from './constants'; * Check if a template is removable. * * @param {Object} template The template entity to check. - * @return {boolean} Whether the template is revertable. + * @return {boolean} Whether the template is removable. */ export default function isTemplateRemovable( template ) { if ( ! template ) { @@ -15,6 +15,8 @@ export default function isTemplateRemovable( template ) { } return ( - template.source === TEMPLATE_ORIGINS.custom && ! template.has_theme_file + template.source === TEMPLATE_ORIGINS.custom && + ! Boolean( template.plugin ) && + ! template.has_theme_file ); } diff --git a/packages/edit-site/src/utils/is-template-revertable.js b/packages/edit-site/src/utils/is-template-revertable.js index a6274d07ebebb6..42413b06cd48ec 100644 --- a/packages/edit-site/src/utils/is-template-revertable.js +++ b/packages/edit-site/src/utils/is-template-revertable.js @@ -15,7 +15,8 @@ export default function isTemplateRevertable( template ) { } /* eslint-disable camelcase */ return ( - template?.source === TEMPLATE_ORIGINS.custom && template?.has_theme_file + template?.source === TEMPLATE_ORIGINS.custom && + ( Boolean( template?.plugin ) || template?.has_theme_file ) ); /* eslint-enable camelcase */ } diff --git a/packages/editor/src/dataviews/actions/reset-post.tsx b/packages/editor/src/dataviews/actions/reset-post.tsx index 59199555ddd4db..cc4cea8f5c82c0 100644 --- a/packages/editor/src/dataviews/actions/reset-post.tsx +++ b/packages/editor/src/dataviews/actions/reset-post.tsx @@ -32,7 +32,8 @@ const resetPost: Action< Post > = { return ( isTemplateOrTemplatePart( item ) && item?.source === TEMPLATE_ORIGINS.custom && - item?.has_theme_file + ( Boolean( item.type === 'wp_template' && item?.plugin ) || + item?.has_theme_file ) ); }, icon: backup, diff --git a/packages/editor/src/dataviews/actions/utils.ts b/packages/editor/src/dataviews/actions/utils.ts index 7da1f71728365b..33a2be16397f3f 100644 --- a/packages/editor/src/dataviews/actions/utils.ts +++ b/packages/editor/src/dataviews/actions/utils.ts @@ -57,6 +57,8 @@ export function isTemplateRemovable( template: Template | TemplatePart ) { return ( [ template.source, template.source ].includes( TEMPLATE_ORIGINS.custom - ) && ! template.has_theme_file + ) && + ! Boolean( template.type === 'wp_template' && template?.plugin ) && + ! template.has_theme_file ); } diff --git a/packages/editor/src/dataviews/types.ts b/packages/editor/src/dataviews/types.ts index 514953d6691290..d207410ca2b6a5 100644 --- a/packages/editor/src/dataviews/types.ts +++ b/packages/editor/src/dataviews/types.ts @@ -34,6 +34,8 @@ export interface Template extends CommonPost { type: 'wp_template'; is_custom: boolean; source: string; + origin: string; + plugin?: string; has_theme_file: boolean; id: string; } @@ -41,6 +43,7 @@ export interface Template extends CommonPost { export interface TemplatePart extends CommonPost { type: 'wp_template_part'; source: string; + origin: string; has_theme_file: boolean; id: string; area: string; diff --git a/packages/editor/src/store/private-actions.js b/packages/editor/src/store/private-actions.js index 0996d6eb8b9d32..e22929011256d5 100644 --- a/packages/editor/src/store/private-actions.js +++ b/packages/editor/src/store/private-actions.js @@ -269,7 +269,7 @@ export const revertTemplate = const fileTemplatePath = addQueryArgs( `${ templateEntityConfig.baseURL }/${ template.id }`, - { context: 'edit', source: 'theme' } + { context: 'edit', source: template.origin } ); const fileTemplate = await apiFetch( { path: fileTemplatePath } ); diff --git a/packages/editor/src/store/utils/is-template-revertable.js b/packages/editor/src/store/utils/is-template-revertable.js index a09715af875bc2..2cb674920e3e4c 100644 --- a/packages/editor/src/store/utils/is-template-revertable.js +++ b/packages/editor/src/store/utils/is-template-revertable.js @@ -18,6 +18,7 @@ export default function isTemplateRevertable( templateOrTemplatePart ) { return ( templateOrTemplatePart.source === TEMPLATE_ORIGINS.custom && - templateOrTemplatePart.has_theme_file + ( Boolean( templateOrTemplatePart?.plugin ) || + templateOrTemplatePart?.has_theme_file ) ); } diff --git a/phpunit/block-template-test.php b/phpunit/block-template-test.php new file mode 100644 index 00000000000000..6589aad90b8053 --- /dev/null +++ b/phpunit/block-template-test.php @@ -0,0 +1,37 @@ +assertArrayHasKey( $template_name, $templates ); + + wp_unregister_block_template( $template_name ); + } + + public function test_get_block_template_from_registry() { + $template_name = 'test-plugin//test-template'; + $args = array( + 'title' => 'Test Template', + ); + + wp_register_block_template( $template_name, $args ); + + $template = get_block_template( 'block-theme//test-template' ); + + $this->assertEquals( 'Test Template', $template->title ); + + wp_unregister_block_template( $template_name ); + } +} diff --git a/phpunit/class-gutenberg-rest-templates-controller-test.php b/phpunit/class-gutenberg-rest-templates-controller-test.php new file mode 100644 index 00000000000000..14735246c6fb20 --- /dev/null +++ b/phpunit/class-gutenberg-rest-templates-controller-test.php @@ -0,0 +1,119 @@ +user->create( + array( + 'role' => 'administrator', + ) + ); + } + + public static function wpTearDownAfterClass() { + self::delete_user( self::$admin_id ); + } + + public function test_get_item() { + wp_set_current_user( self::$admin_id ); + + $template_name = 'test-plugin//test-template'; + $args = array( + 'content' => 'Template content', + 'title' => 'Test Template', + 'description' => 'Description of test template', + 'post_types' => array( 'post', 'page' ), + ); + + wp_register_block_template( $template_name, $args ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/templates/test-plugin//test-template' ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertNotWPError( $response, "Fetching a registered template shouldn't cause an error." ); + + $data = $response->get_data(); + + $this->assertSame( 'default//test-template', $data['id'], 'Template ID mismatch.' ); + $this->assertSame( 'default', $data['theme'], 'Template theme mismatch.' ); + $this->assertSame( 'Template content', $data['content']['raw'], 'Template content mismatch.' ); + $this->assertSame( 'test-template', $data['slug'], 'Template slug mismatch.' ); + $this->assertSame( 'plugin', $data['source'], "Template source should be 'plugin'." ); + $this->assertSame( 'plugin', $data['origin'], "Template origin should be 'plugin'." ); + $this->assertSame( 'test-plugin', $data['author_text'], 'Template author text mismatch.' ); + $this->assertSame( 'Description of test template', $data['description'], 'Template description mismatch.' ); + $this->assertSame( 'Test Template', $data['title']['rendered'], 'Template title mismatch.' ); + $this->assertSame( 'test-plugin', $data['plugin'], 'Plugin name mismatch.' ); + + wp_unregister_block_template( $template_name ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/templates/test-plugin//test-template' ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertNotWPError( $response, "Fetching an unregistered template shouldn't cause an error." ); + $this->assertSame( 404, $response->get_status(), 'Fetching an unregistered template should return 404.' ); + } + + /** + * @doesNotPerformAssertions + */ + public function test_register_routes() { + // Already present in core test class: Tests_REST_WpRestTemplatesController. + } + + /** + * @doesNotPerformAssertions + */ + public function test_get_items() { + // Already present in core test class: Tests_REST_WpRestTemplatesController. + } + + /** + * @doesNotPerformAssertions + */ + public function test_create_item() { + // Already present in core test class: Tests_REST_WpRestTemplatesController. + } + + /** + * @doesNotPerformAssertions + */ + public function test_update_item() { + // Already present in core test class: Tests_REST_WpRestTemplatesController. + } + + /** + * @doesNotPerformAssertions + */ + public function test_delete_item() { + // Already present in core test class: Tests_REST_WpRestTemplatesController. + } + + /** + * @doesNotPerformAssertions + */ + public function test_context_param() { + // Already present in core test class: Tests_REST_WpRestTemplatesController. + } + + /** + * @doesNotPerformAssertions + */ + public function test_prepare_item() { + // Already present in core test class: Tests_REST_WpRestTemplatesController. + } + + /** + * @doesNotPerformAssertions + */ + public function test_get_item_schema() { + // Already present in core test class: Tests_REST_WpRestTemplatesController. + } +} diff --git a/phpunit/class-wp-block-templates-registry-test.php b/phpunit/class-wp-block-templates-registry-test.php new file mode 100644 index 00000000000000..fb8436eb6153d4 --- /dev/null +++ b/phpunit/class-wp-block-templates-registry-test.php @@ -0,0 +1,192 @@ +register( $template_name ); + + $this->assertSame( $template->slug, 'test-template' ); + + self::$registry->unregister( $template_name ); + } + + public function test_register_template_invalid_name() { + // Try to register a template with invalid name (non-string). + $template_name = array( 'invalid-template-name' ); + + $this->setExpectedIncorrectUsage( 'WP_Block_Templates_Registry::register' ); + $result = self::$registry->register( $template_name ); + + $this->assertWPError( $result ); + $this->assertSame( 'template_name_no_string', $result->get_error_code(), 'Error code mismatch.' ); + $this->assertSame( 'Template names must be strings.', $result->get_error_message(), 'Error message mismatch.' ); + } + + public function test_register_template_invalid_name_uppercase() { + // Try to register a template with uppercase characters in the name. + $template_name = 'test-plugin//Invalid-Template-Name'; + + $this->setExpectedIncorrectUsage( 'WP_Block_Templates_Registry::register' ); + $result = self::$registry->register( $template_name ); + + $this->assertWPError( $result ); + $this->assertSame( 'template_name_no_uppercase', $result->get_error_code(), 'Error code mismatch.' ); + $this->assertSame( 'Template names must not contain uppercase characters.', $result->get_error_message(), 'Error message mismatch.' ); + } + + public function test_register_template_no_prefix() { + // Try to register a template without a namespace. + $this->setExpectedIncorrectUsage( 'WP_Block_Templates_Registry::register' ); + $result = self::$registry->register( 'template-no-plugin', array() ); + + $this->assertWPError( $result ); + $this->assertSame( 'template_no_prefix', $result->get_error_code(), 'Error code mismatch.' ); + $this->assertSame( 'Template names must contain a namespace prefix. Example: my-plugin//my-custom-template', $result->get_error_message(), 'Error message mismatch.' ); + } + + public function test_register_template_already_exists() { + // Register the template for the first time. + $template_name = 'test-plugin//duplicate-template'; + self::$registry->register( $template_name ); + + // Try to register the same template again. + $this->setExpectedIncorrectUsage( 'WP_Block_Templates_Registry::register' ); + $result = self::$registry->register( $template_name ); + + $this->assertWPError( $result ); + $this->assertSame( 'template_already_registered', $result->get_error_code(), 'Error code mismatch.' ); + $this->assertStringContainsString( 'Template "test-plugin//duplicate-template" is already registered.', $result->get_error_message(), 'Error message mismatch.' ); + + self::$registry->unregister( $template_name ); + } + + public function test_get_all_registered() { + $template_name_1 = 'test-plugin//template-1'; + $template_name_2 = 'test-plugin//template-2'; + self::$registry->register( $template_name_1 ); + self::$registry->register( $template_name_2 ); + + $all_templates = self::$registry->get_all_registered(); + + $this->assertIsArray( $all_templates, 'Registered templates should be an array.' ); + $this->assertCount( 2, $all_templates, 'Registered templates should contain 2 items.' ); + $this->assertArrayHasKey( 'test-plugin//template-1', $all_templates, 'Registered templates should contain "test-plugin//template-1".' ); + $this->assertArrayHasKey( 'test-plugin//template-2', $all_templates, 'Registered templates should contain "test-plugin//template-2".' ); + + self::$registry->unregister( $template_name_1 ); + self::$registry->unregister( $template_name_2 ); + } + + public function test_get_registered() { + $template_name = 'test-plugin//registered-template'; + $args = array( + 'content' => 'Template content', + 'title' => 'Registered Template', + 'description' => 'Description of registered template', + 'post_types' => array( 'post', 'page' ), + ); + self::$registry->register( $template_name, $args ); + + $registered_template = self::$registry->get_registered( $template_name ); + + $this->assertSame( 'default', $registered_template->theme, 'Template theme mismatch.' ); + $this->assertSame( 'registered-template', $registered_template->slug, 'Template slug mismatch.' ); + $this->assertSame( 'default//registered-template', $registered_template->id, 'Template ID mismatch.' ); + $this->assertSame( 'Registered Template', $registered_template->title, 'Template title mismatch.' ); + $this->assertSame( 'Template content', $registered_template->content, 'Template content mismatch.' ); + $this->assertSame( 'Description of registered template', $registered_template->description, 'Template description mismatch.' ); + $this->assertSame( 'plugin', $registered_template->source, "Template source should be 'plugin'." ); + $this->assertSame( 'plugin', $registered_template->origin, "Template origin should be 'plugin'." ); + $this->assertEquals( array( 'post', 'page' ), $registered_template->post_types, 'Template post types mismatch.' ); + $this->assertSame( 'test-plugin', $registered_template->plugin, 'Plugin name mismatch.' ); + + self::$registry->unregister( $template_name ); + } + + public function test_get_by_slug() { + $slug = 'slug-template'; + $template_name = 'test-plugin//' . $slug; + $args = array( + 'content' => 'Template content', + 'title' => 'Slug Template', + ); + self::$registry->register( $template_name, $args ); + + $registered_template = self::$registry->get_by_slug( $slug ); + + $this->assertNotNull( $registered_template, 'Registered template should not be null.' ); + $this->assertSame( $slug, $registered_template->slug, 'Template slug mismatch.' ); + + self::$registry->unregister( $template_name ); + } + + public function test_get_by_query() { + $template_name_1 = 'test-plugin//query-template-1'; + $template_name_2 = 'test-plugin//query-template-2'; + $args_1 = array( + 'content' => 'Template content 1', + 'title' => 'Query Template 1', + ); + $args_2 = array( + 'content' => 'Template content 2', + 'title' => 'Query Template 2', + ); + self::$registry->register( $template_name_1, $args_1 ); + self::$registry->register( $template_name_2, $args_2 ); + + $query = array( + 'slug__in' => array( 'query-template-1' ), + ); + $results = self::$registry->get_by_query( $query ); + + $this->assertCount( 1, $results, 'Query result should contain 1 item.' ); + $this->assertArrayHasKey( $template_name_1, $results, 'Query result should contain "test-plugin//query-template-1".' ); + + self::$registry->unregister( $template_name_1 ); + self::$registry->unregister( $template_name_2 ); + } + + public function test_is_registered() { + $template_name = 'test-plugin//is-registered-template'; + $args = array( + 'content' => 'Template content', + 'title' => 'Is Registered Template', + ); + self::$registry->register( $template_name, $args ); + + $this->assertTrue( self::$registry->is_registered( $template_name ) ); + + self::$registry->unregister( $template_name ); + } + + public function test_unregister() { + $template_name = 'test-plugin//unregister-template'; + $args = array( + 'content' => 'Template content', + 'title' => 'Unregister Template', + ); + $template = self::$registry->register( $template_name, $args ); + + $unregistered_template = self::$registry->unregister( $template_name ); + + $this->assertEquals( $template, $unregistered_template, 'Unregistered template should be the same as the registered one.' ); + $this->assertFalse( self::$registry->is_registered( $template_name ), 'Template should not be registered after unregistering.' ); + } +} diff --git a/test/e2e/specs/site-editor/template-registration.spec.js b/test/e2e/specs/site-editor/template-registration.spec.js new file mode 100644 index 00000000000000..132e3a8c49a902 --- /dev/null +++ b/test/e2e/specs/site-editor/template-registration.spec.js @@ -0,0 +1,356 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +test.use( { + blockTemplateRegistrationUtils: async ( { editor, page }, use ) => { + await use( new BlockTemplateRegistrationUtils( { editor, page } ) ); + }, +} ); + +test.describe( 'Block template registration', () => { + test.beforeAll( async ( { requestUtils } ) => { + await requestUtils.activateTheme( 'emptytheme' ); + await requestUtils.activatePlugin( + 'gutenberg-test-block-template-registration' + ); + } ); + test.afterAll( async ( { requestUtils } ) => { + await requestUtils.deactivatePlugin( + 'gutenberg-test-block-template-registration' + ); + } ); + test.afterEach( async ( { requestUtils } ) => { + await requestUtils.deleteAllTemplates( 'wp_template' ); + await requestUtils.deleteAllPosts(); + } ); + + test( 'templates can be registered and edited', async ( { + admin, + editor, + page, + blockTemplateRegistrationUtils, + } ) => { + // Verify template is applied to the frontend. + await page.goto( '/?cat=1' ); + await expect( + page.getByText( 'This is a plugin-registered template.' ) + ).toBeVisible(); + + // Verify template is listed in the Site Editor. + await admin.visitSiteEditor( { + postType: 'wp_template', + } ); + await blockTemplateRegistrationUtils.searchForTemplate( + 'Plugin Template' + ); + await expect( page.getByText( 'Plugin Template' ) ).toBeVisible(); + await expect( + page.getByText( 'A template registered by a plugin.' ) + ).toBeVisible(); + await expect( page.getByText( 'AuthorGutenberg' ) ).toBeVisible(); + + // Verify the template contents are rendered in the editor. + await page.getByText( 'Plugin Template' ).click(); + await expect( + editor.canvas.getByText( 'This is a plugin-registered template.' ) + ).toBeVisible(); + + // Verify edits persist in the frontend. + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { content: 'User-edited template' }, + } ); + await editor.saveSiteEditorEntities( { + isOnlyCurrentEntityDirty: true, + } ); + await page.goto( '/?cat=1' ); + await expect( page.getByText( 'User-edited template' ) ).toBeVisible(); + + // Verify template can be reset. + await admin.visitSiteEditor( { + postType: 'wp_template', + } ); + const resetNotice = page + .getByLabel( 'Dismiss this notice' ) + .getByText( `"Plugin Template" reset.` ); + const savedButton = page.getByRole( 'button', { + name: 'Saved', + } ); + await blockTemplateRegistrationUtils.searchForTemplate( + 'Plugin Template' + ); + const searchResults = page.getByLabel( 'Actions' ); + await searchResults.first().click(); + await page.getByRole( 'menuitem', { name: 'Reset' } ).click(); + await page.getByRole( 'button', { name: 'Reset' } ).click(); + + await expect( resetNotice ).toBeVisible(); + await expect( savedButton ).toBeVisible(); + await page.goto( '/?cat=1' ); + await expect( + page.getByText( 'Content edited template.' ) + ).toBeHidden(); + } ); + + test( 'registered templates are available in the Swap template screen', async ( { + admin, + editor, + page, + } ) => { + // Create a post. + await admin.visitAdminPage( '/post-new.php' ); + await page.getByLabel( 'Close', { exact: true } ).click(); + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { content: 'User-created post.' }, + } ); + + // Swap template. + await page.getByRole( 'button', { name: 'Post' } ).click(); + await page.getByRole( 'button', { name: 'Template options' } ).click(); + await page.getByRole( 'menuitem', { name: 'Swap template' } ).click(); + await page.getByText( 'Plugin Template' ).click(); + + // Verify the template is applied. + const postId = await editor.publishPost(); + await page.goto( `?p=${ postId }` ); + await expect( + page.getByText( 'This is a plugin-registered template.' ) + ).toBeVisible(); + } ); + + test( 'themes can override registered templates', async ( { + admin, + editor, + page, + blockTemplateRegistrationUtils, + } ) => { + // Create a post. + await admin.visitAdminPage( '/post-new.php' ); + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { content: 'User-created post.' }, + } ); + + // Swap template. + await page.getByRole( 'button', { name: 'Post' } ).click(); + await page.getByRole( 'button', { name: 'Template options' } ).click(); + await page.getByRole( 'menuitem', { name: 'Swap template' } ).click(); + await page.getByText( 'Custom', { exact: true } ).click(); + + // Verify the theme template is applied. + const postId = await editor.publishPost(); + await page.goto( `?p=${ postId }` ); + await expect( + page.getByText( 'Custom template for Posts' ) + ).toBeVisible(); + await expect( + page.getByText( + 'This is a plugin-registered template and overridden by a theme.' + ) + ).toBeHidden(); + + // Verify the plugin-registered template doesn't appear in the Site Editor. + await admin.visitSiteEditor( { + postType: 'wp_template', + } ); + await blockTemplateRegistrationUtils.searchForTemplate( 'Custom' ); + await expect( + page.getByText( 'Custom Template (overridden by the theme)' ) + ).toBeHidden(); + // Verify the theme template shows the theme name as the author. + await expect( page.getByText( 'AuthorEmptytheme' ) ).toBeVisible(); + } ); + + test( 'templates can be deleted if the registered plugin is deactivated', async ( { + admin, + editor, + page, + requestUtils, + blockTemplateRegistrationUtils, + } ) => { + // Make an edit to the template. + await admin.visitSiteEditor( { + postType: 'wp_template', + } ); + await blockTemplateRegistrationUtils.searchForTemplate( + 'Plugin Template' + ); + await page.getByText( 'Plugin Template' ).click(); + await expect( + editor.canvas.getByText( 'This is a plugin-registered template.' ) + ).toBeVisible(); + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { content: 'User-customized template' }, + } ); + await editor.saveSiteEditorEntities( { + isOnlyCurrentEntityDirty: true, + } ); + + // Deactivate plugin. + await requestUtils.deactivatePlugin( + 'gutenberg-test-block-template-registration' + ); + + // Verify template can be deleted. + await admin.visitSiteEditor( { + postType: 'wp_template', + } ); + const deletedNotice = page + .getByLabel( 'Dismiss this notice' ) + .getByText( `"Plugin Template" deleted.` ); + const savedButton = page.getByRole( 'button', { + name: 'Saved', + } ); + await blockTemplateRegistrationUtils.searchForTemplate( + 'Plugin Template' + ); + const searchResults = page.getByLabel( 'Actions' ); + await searchResults.first().click(); + await page.getByRole( 'menuitem', { name: 'Delete' } ).click(); + await page.getByRole( 'button', { name: 'Delete' } ).click(); + + await expect( deletedNotice ).toBeVisible(); + await expect( savedButton ).toBeVisible(); + + // Expect template to no longer appear in the Site Editor. + await expect( page.getByLabel( 'Actions' ) ).toBeHidden(); + + // Reactivate plugin. + await requestUtils.activatePlugin( + 'gutenberg-test-block-template-registration' + ); + } ); + + test( 'registered templates can be unregistered', async ( { + admin, + page, + blockTemplateRegistrationUtils, + } ) => { + await admin.visitSiteEditor( { + postType: 'wp_template', + } ); + await blockTemplateRegistrationUtils.searchForTemplate( + 'Plugin Unregistered Template' + ); + await expect( + page.getByText( 'Plugin Unregistered Template' ) + ).toBeHidden(); + } ); + + test( 'WP default templates can be overridden by plugins', async ( { + page, + } ) => { + await page.goto( '?page_id=2' ); + await expect( + page.getByText( 'This is a plugin-registered page template.' ) + ).toBeVisible(); + } ); + + test( 'user-customized templates cannot be overridden by plugins', async ( { + admin, + editor, + page, + requestUtils, + blockTemplateRegistrationUtils, + } ) => { + await requestUtils.deactivatePlugin( + 'gutenberg-test-block-template-registration' + ); + + // Create an author template. + await admin.visitSiteEditor( { + postType: 'wp_template', + } ); + await page.getByLabel( 'Add New Template' ).click(); + await page.getByRole( 'button', { name: 'Author Archives' } ).click(); + await page + .getByRole( 'button', { name: 'Author For a specific item' } ) + .click(); + await page.getByRole( 'option', { name: 'admin' } ).click(); + await expect( page.getByText( 'Choose a pattern' ) ).toBeVisible(); + await page.getByLabel( 'Close', { exact: true } ).click(); + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { content: 'Author template customized by the user.' }, + } ); + await editor.saveSiteEditorEntities( { + isOnlyCurrentEntityDirty: true, + } ); + + await requestUtils.activatePlugin( + 'gutenberg-test-block-template-registration' + ); + + // Verify the template edited by the user has priority over the one registered by the theme. + await page.goto( '?author=1' ); + await expect( + page.getByText( 'Author template customized by the user.' ) + ).toBeVisible(); + await expect( + page.getByText( 'This is a plugin-registered author template.' ) + ).toBeHidden(); + + // Verify the template registered by the plugin is not visible in the Site Editor. + await admin.visitSiteEditor( { + postType: 'wp_template', + } ); + await blockTemplateRegistrationUtils.searchForTemplate( + 'Plugin Author Template' + ); + await expect( page.getByText( 'Plugin Author Template' ) ).toBeHidden(); + + // Reset the user-modified template. + const resetNotice = page + .getByLabel( 'Dismiss this notice' ) + .getByText( `"Author: Admin" reset.` ); + await page.getByPlaceholder( 'Search' ).fill( 'Author: admin' ); + await page.getByRole( 'link', { name: 'Author: Admin' } ).click(); + const actions = page.getByLabel( 'Actions' ); + await actions.first().click(); + await page.getByRole( 'menuitem', { name: 'Reset' } ).click(); + await page.getByRole( 'button', { name: 'Reset' } ).click(); + + await expect( resetNotice ).toBeVisible(); + + // Verify the template registered by the plugin is applied in the editor... + await expect( + editor.canvas.getByText( 'Author template customized by the user.' ) + ).toBeHidden(); + await expect( + editor.canvas.getByText( + 'This is a plugin-registered author template.' + ) + ).toBeVisible(); + + // ... and the frontend. + await page.goto( '?author=1' ); + await expect( + page.getByText( 'Author template customized by the user.' ) + ).toBeHidden(); + await expect( + page.getByText( 'This is a plugin-registered author template.' ) + ).toBeVisible(); + } ); +} ); + +class BlockTemplateRegistrationUtils { + constructor( { page } ) { + this.page = page; + } + + async searchForTemplate( searchTerm ) { + const searchResults = this.page.getByLabel( 'Actions' ); + await expect + .poll( async () => await searchResults.count() ) + .toBeGreaterThan( 0 ); + const initialSearchResultsCount = await searchResults.count(); + await this.page.getByPlaceholder( 'Search' ).fill( searchTerm ); + await expect + .poll( async () => await searchResults.count() ) + .toBeLessThan( initialSearchResultsCount ); + } +} From 7502c6a39ccb50df5fc1be7c11bac4d8e04aa48b Mon Sep 17 00:00:00 2001 From: Mario Santos <34552881+SantosGuillamot@users.noreply.github.com> Date: Tue, 13 Aug 2024 12:13:46 +0200 Subject: [PATCH 062/126] Fit items into 100% width (#64465) Fix long keys overflow in bindings panel Co-authored-by: SantosGuillamot Co-authored-by: jasmussen --- packages/block-editor/src/hooks/block-bindings.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/block-editor/src/hooks/block-bindings.scss b/packages/block-editor/src/hooks/block-bindings.scss index 73e7c490160d3e..603b2115623b8f 100644 --- a/packages/block-editor/src/hooks/block-bindings.scss +++ b/packages/block-editor/src/hooks/block-bindings.scss @@ -1,5 +1,5 @@ div.block-editor-bindings__panel { - grid-template-columns: auto; + grid-template-columns: repeat(auto-fit, minmax(100%, 1fr)); button:hover .block-editor-bindings__item-explanation { color: inherit; } From ea0bcf1ce1345afc840595040a6f4bf9be5faf9c Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Tue, 13 Aug 2024 12:35:17 +0200 Subject: [PATCH 063/126] DataViews Extensibility: Allow unregistering the view post revisions action (#64464) Co-authored-by: youknowriad Co-authored-by: ntsekouras --- packages/dataviews/src/types.ts | 6 +++ .../src/components/post-actions/actions.js | 40 +--------------- .../dataviews/actions/view-post-revisions.tsx | 47 +++++++++++++++++++ .../src/dataviews/store/private-actions.ts | 4 ++ packages/editor/src/dataviews/types.ts | 9 +++- 5 files changed, 66 insertions(+), 40 deletions(-) create mode 100644 packages/editor/src/dataviews/actions/view-post-revisions.tsx diff --git a/packages/dataviews/src/types.ts b/packages/dataviews/src/types.ts index 7bbbc8cb863c09..fa5cec8d7d0320 100644 --- a/packages/dataviews/src/types.ts +++ b/packages/dataviews/src/types.ts @@ -426,6 +426,12 @@ interface ActionBase< Item > { * Whether the action can be used as a bulk action. */ supportsBulk?: boolean; + + /** + * The context in which the action is visible. + * This is only a "meta" information for now. + */ + context?: 'list' | 'single'; } export interface RenderModalProps< Item > { diff --git a/packages/editor/src/components/post-actions/actions.js b/packages/editor/src/components/post-actions/actions.js index 627de5e8652e47..831a4f5349869b 100644 --- a/packages/editor/src/components/post-actions/actions.js +++ b/packages/editor/src/components/post-actions/actions.js @@ -2,10 +2,9 @@ * WordPress dependencies */ import { external } from '@wordpress/icons'; -import { addQueryArgs } from '@wordpress/url'; import { useDispatch, useSelect } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; -import { __, sprintf } from '@wordpress/i18n'; +import { __ } from '@wordpress/i18n'; import { useMemo, useEffect } from '@wordpress/element'; /** @@ -31,40 +30,6 @@ const viewPostAction = { }, }; -const postRevisionsAction = { - id: 'view-post-revisions', - context: 'list', - label( items ) { - const revisionsCount = - items[ 0 ]._links?.[ 'version-history' ]?.[ 0 ]?.count ?? 0; - return sprintf( - /* translators: %s: number of revisions */ - __( 'View revisions (%s)' ), - revisionsCount - ); - }, - isEligible: ( post ) => { - if ( post.status === 'trash' ) { - return false; - } - const lastRevisionId = - post?._links?.[ 'predecessor-version' ]?.[ 0 ]?.id ?? null; - const revisionsCount = - post?._links?.[ 'version-history' ]?.[ 0 ]?.count ?? 0; - return lastRevisionId && revisionsCount > 1; - }, - callback( posts, { onActionPerformed } ) { - const post = posts[ 0 ]; - const href = addQueryArgs( 'revision.php', { - revision: post?._links?.[ 'predecessor-version' ]?.[ 0 ]?.id, - } ); - document.location.href = href; - if ( onActionPerformed ) { - onActionPerformed( posts ); - } - }, -}; - export function usePostActions( { postType, onActionPerformed, context } ) { const { defaultActions, postTypeObject } = useSelect( ( select ) => { @@ -84,7 +49,6 @@ export function usePostActions( { postType, onActionPerformed, context } ) { }, [ registerPostTypeActions, postType ] ); const isLoaded = !! postTypeObject; - const supportsRevisions = !! postTypeObject?.supports?.revisions; return useMemo( () => { if ( ! isLoaded ) { return []; @@ -92,7 +56,6 @@ export function usePostActions( { postType, onActionPerformed, context } ) { let actions = [ postTypeObject?.viewable && viewPostAction, - supportsRevisions && postRevisionsAction, ...defaultActions, ].filter( Boolean ); // Filter actions based on provided context. If not provided @@ -161,7 +124,6 @@ export function usePostActions( { postType, onActionPerformed, context } ) { postTypeObject?.viewable, onActionPerformed, isLoaded, - supportsRevisions, context, ] ); } diff --git a/packages/editor/src/dataviews/actions/view-post-revisions.tsx b/packages/editor/src/dataviews/actions/view-post-revisions.tsx new file mode 100644 index 00000000000000..875b925b94f070 --- /dev/null +++ b/packages/editor/src/dataviews/actions/view-post-revisions.tsx @@ -0,0 +1,47 @@ +/** + * WordPress dependencies + */ +import { addQueryArgs } from '@wordpress/url'; +import { __, sprintf } from '@wordpress/i18n'; +import type { Action } from '@wordpress/dataviews'; + +/** + * Internal dependencies + */ +import type { Post } from '../types'; + +const viewPostRevisions: Action< Post > = { + id: 'view-post-revisions', + context: 'list', + label( items ) { + const revisionsCount = + items[ 0 ]._links?.[ 'version-history' ]?.[ 0 ]?.count ?? 0; + return sprintf( + /* translators: %s: number of revisions */ + __( 'View revisions (%s)' ), + revisionsCount + ); + }, + isEligible( post ) { + if ( post.status === 'trash' ) { + return false; + } + const lastRevisionId = + post?._links?.[ 'predecessor-version' ]?.[ 0 ]?.id ?? null; + const revisionsCount = + post?._links?.[ 'version-history' ]?.[ 0 ]?.count ?? 0; + return !! lastRevisionId && revisionsCount > 1; + }, + callback( posts, { onActionPerformed } ) { + const post = posts[ 0 ]; + const href = addQueryArgs( 'revision.php', { + revision: post?._links?.[ 'predecessor-version' ]?.[ 0 ]?.id, + } ); + document.location.href = href; + if ( onActionPerformed ) { + onActionPerformed( posts ); + } + }, +}; + +export default viewPostRevisions; diff --git a/packages/editor/src/dataviews/store/private-actions.ts b/packages/editor/src/dataviews/store/private-actions.ts index d5b299b012e364..3ac121aea7393b 100644 --- a/packages/editor/src/dataviews/store/private-actions.ts +++ b/packages/editor/src/dataviews/store/private-actions.ts @@ -22,6 +22,7 @@ import type { PostType } from '../types'; import { store as editorStore } from '../../store'; import { unlock } from '../../lock-unlock'; import duplicatePost from '../actions/duplicate-post'; +import viewPostRevisions from '../actions/view-post-revisions'; export function registerEntityAction< Item >( kind: string, @@ -88,6 +89,9 @@ export const registerPostTypeActions = .getCurrentTheme(); const actions = [ + !! postTypeConfig?.supports?.revisions + ? viewPostRevisions + : undefined, // @ts-ignore globalThis.IS_GUTENBERG_PLUGIN ? ! [ 'wp_template', 'wp_block', 'wp_template_part' ].includes( diff --git a/packages/editor/src/dataviews/types.ts b/packages/editor/src/dataviews/types.ts index d207410ca2b6a5..e5886792fb9c33 100644 --- a/packages/editor/src/dataviews/types.ts +++ b/packages/editor/src/dataviews/types.ts @@ -14,6 +14,13 @@ export interface CommonPost { type: string; id: string | number; blocks?: Object[]; + _links?: Links; +} + +interface Links { + 'predecessor-version'?: { href: string; id: number }[]; + 'version-history'?: { href: string; count: number }[]; + [ key: string ]: { href: string }[] | undefined; } export interface BasePost extends CommonPost { @@ -27,7 +34,6 @@ export interface BasePost extends CommonPost { featured_media?: number; menu_order?: number; ping_status?: 'open' | 'closed'; - _links?: Record< string, { href: string }[] >; } export interface Template extends CommonPost { @@ -69,6 +75,7 @@ export interface PostType { supports?: { 'page-attributes'?: boolean; title?: boolean; + revisions?: boolean; }; } From 64643ed8727d915557c4bef65f247ab287d72465 Mon Sep 17 00:00:00 2001 From: George Mamadashvili Date: Tue, 13 Aug 2024 14:59:14 +0400 Subject: [PATCH 064/126] Block Editor: Refactor inner blocks appender components (#64470) * Block Editor: Refactor inner blocks appender components * Remove 'withClientId' HoC Co-authored-by: Mamaduka Co-authored-by: youknowriad Co-authored-by: tyxla --- .../inner-blocks/button-block-appender.js | 12 ++++----- .../inner-blocks/default-block-appender.js | 27 +++---------------- .../components/inner-blocks/with-client-id.js | 19 ------------- 3 files changed, 9 insertions(+), 49 deletions(-) delete mode 100644 packages/block-editor/src/components/inner-blocks/with-client-id.js diff --git a/packages/block-editor/src/components/inner-blocks/button-block-appender.js b/packages/block-editor/src/components/inner-blocks/button-block-appender.js index 500e59863db429..5bc788d58582f6 100644 --- a/packages/block-editor/src/components/inner-blocks/button-block-appender.js +++ b/packages/block-editor/src/components/inner-blocks/button-block-appender.js @@ -7,15 +7,15 @@ import clsx from 'clsx'; * Internal dependencies */ import BaseButtonBlockAppender from '../button-block-appender'; -import withClientId from './with-client-id'; +import { useBlockEditContext } from '../block-edit/context'; -export const ButtonBlockAppender = ( { - clientId, +export default function ButtonBlockAppender( { showSeparator, isFloating, onAddBlock, isToggle, -} ) => { +} ) { + const { clientId } = useBlockEditContext(); return ( ); -}; - -export default withClientId( ButtonBlockAppender ); +} diff --git a/packages/block-editor/src/components/inner-blocks/default-block-appender.js b/packages/block-editor/src/components/inner-blocks/default-block-appender.js index d2e137004d83bf..91e48a2854b513 100644 --- a/packages/block-editor/src/components/inner-blocks/default-block-appender.js +++ b/packages/block-editor/src/components/inner-blocks/default-block-appender.js @@ -1,29 +1,10 @@ -/** - * WordPress dependencies - */ -import { compose } from '@wordpress/compose'; -import { withSelect } from '@wordpress/data'; - /** * Internal dependencies */ import BaseDefaultBlockAppender from '../default-block-appender'; -import withClientId from './with-client-id'; -import { store as blockEditorStore } from '../../store'; +import { useBlockEditContext } from '../block-edit/context'; -export const DefaultBlockAppender = ( { clientId } ) => { +export default function DefaultBlockAppender() { + const { clientId } = useBlockEditContext(); return ; -}; - -export default compose( [ - withClientId, - withSelect( ( select, { clientId } ) => { - const { getBlockOrder } = select( blockEditorStore ); - - const blockClientIds = getBlockOrder( clientId ); - - return { - lastBlockClientId: blockClientIds[ blockClientIds.length - 1 ], - }; - } ), -] )( DefaultBlockAppender ); +} diff --git a/packages/block-editor/src/components/inner-blocks/with-client-id.js b/packages/block-editor/src/components/inner-blocks/with-client-id.js deleted file mode 100644 index 97c73ae2803934..00000000000000 --- a/packages/block-editor/src/components/inner-blocks/with-client-id.js +++ /dev/null @@ -1,19 +0,0 @@ -/** - * WordPress dependencies - */ -import { createHigherOrderComponent } from '@wordpress/compose'; - -/** - * Internal dependencies - */ -import { useBlockEditContext } from '../block-edit/context'; - -const withClientId = createHigherOrderComponent( - ( WrappedComponent ) => ( props ) => { - const { clientId } = useBlockEditContext(); - return ; - }, - 'withClientId' -); - -export default withClientId; From e8b45dacc23052952e01b4869027c9354818d74a Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Tue, 13 Aug 2024 13:14:17 +0200 Subject: [PATCH 065/126] DataViews Extensibility: Allow unregistering the view post action (#64467) Co-authored-by: youknowriad Co-authored-by: Mamaduka --- .../src/components/post-actions/actions.js | 43 ++----------------- .../src/dataviews/actions/view-post.tsx | 30 +++++++++++++ .../src/dataviews/store/private-actions.ts | 11 +++-- packages/editor/src/dataviews/types.ts | 2 + 4 files changed, 42 insertions(+), 44 deletions(-) create mode 100644 packages/editor/src/dataviews/actions/view-post.tsx diff --git a/packages/editor/src/components/post-actions/actions.js b/packages/editor/src/components/post-actions/actions.js index 831a4f5349869b..e1c0ed1558193d 100644 --- a/packages/editor/src/components/post-actions/actions.js +++ b/packages/editor/src/components/post-actions/actions.js @@ -1,10 +1,7 @@ /** * WordPress dependencies */ -import { external } from '@wordpress/icons'; import { useDispatch, useSelect } from '@wordpress/data'; -import { store as coreStore } from '@wordpress/core-data'; -import { __ } from '@wordpress/i18n'; import { useMemo, useEffect } from '@wordpress/element'; /** @@ -13,30 +10,11 @@ import { useMemo, useEffect } from '@wordpress/element'; import { store as editorStore } from '../../store'; import { unlock } from '../../lock-unlock'; -const viewPostAction = { - id: 'view-post', - label: __( 'View' ), - isPrimary: true, - icon: external, - isEligible( post ) { - return post.status !== 'trash'; - }, - callback( posts, { onActionPerformed } ) { - const post = posts[ 0 ]; - window.open( post.link, '_blank' ); - if ( onActionPerformed ) { - onActionPerformed( posts ); - } - }, -}; - export function usePostActions( { postType, onActionPerformed, context } ) { - const { defaultActions, postTypeObject } = useSelect( + const { defaultActions } = useSelect( ( select ) => { - const { getPostType } = select( coreStore ); const { getEntityActions } = unlock( select( editorStore ) ); return { - postTypeObject: getPostType( postType ), defaultActions: getEntityActions( 'postType', postType ), }; }, @@ -48,23 +26,14 @@ export function usePostActions( { postType, onActionPerformed, context } ) { registerPostTypeActions( postType ); }, [ registerPostTypeActions, postType ] ); - const isLoaded = !! postTypeObject; return useMemo( () => { - if ( ! isLoaded ) { - return []; - } - - let actions = [ - postTypeObject?.viewable && viewPostAction, - ...defaultActions, - ].filter( Boolean ); // Filter actions based on provided context. If not provided // all actions are returned. We'll have a single entry for getting the actions // and the consumer should provide the context to filter the actions, if needed. // Actions should also provide the `context` they support, if it's specific, to // compare with the provided context to get all the actions. // Right now the only supported context is `list`. - actions = actions.filter( ( action ) => { + const actions = defaultActions.filter( ( action ) => { if ( ! action.context ) { return true; } @@ -119,11 +88,5 @@ export function usePostActions( { postType, onActionPerformed, context } ) { } return actions; - }, [ - defaultActions, - postTypeObject?.viewable, - onActionPerformed, - isLoaded, - context, - ] ); + }, [ defaultActions, onActionPerformed, context ] ); } diff --git a/packages/editor/src/dataviews/actions/view-post.tsx b/packages/editor/src/dataviews/actions/view-post.tsx new file mode 100644 index 00000000000000..47eb1a66d019ad --- /dev/null +++ b/packages/editor/src/dataviews/actions/view-post.tsx @@ -0,0 +1,30 @@ +/** + * WordPress dependencies + */ +import { external } from '@wordpress/icons'; +import { __ } from '@wordpress/i18n'; +import type { Action } from '@wordpress/dataviews'; + +/** + * Internal dependencies + */ +import type { BasePost } from '../types'; + +const viewPost: Action< BasePost > = { + id: 'view-post', + label: __( 'View' ), + isPrimary: true, + icon: external, + isEligible( post ) { + return post.status !== 'trash'; + }, + callback( posts, { onActionPerformed } ) { + const post = posts[ 0 ]; + window.open( post?.link, '_blank' ); + if ( onActionPerformed ) { + onActionPerformed( posts ); + } + }, +}; + +export default viewPost; diff --git a/packages/editor/src/dataviews/store/private-actions.ts b/packages/editor/src/dataviews/store/private-actions.ts index 3ac121aea7393b..a9101e57dd08b5 100644 --- a/packages/editor/src/dataviews/store/private-actions.ts +++ b/packages/editor/src/dataviews/store/private-actions.ts @@ -23,6 +23,7 @@ import { store as editorStore } from '../../store'; import { unlock } from '../../lock-unlock'; import duplicatePost from '../actions/duplicate-post'; import viewPostRevisions from '../actions/view-post-revisions'; +import viewPost from '../actions/view-post'; export function registerEntityAction< Item >( kind: string, @@ -89,6 +90,7 @@ export const registerPostTypeActions = .getCurrentTheme(); const actions = [ + postTypeConfig.viewable ? viewPost : undefined, !! postTypeConfig?.supports?.revisions ? viewPostRevisions : undefined, @@ -101,9 +103,10 @@ export const registerPostTypeActions = duplicatePost : undefined, postTypeConfig.slug === 'wp_template_part' && - canCreate && - currentTheme?.is_block_theme && - duplicateTemplatePart, + canCreate && + currentTheme?.is_block_theme + ? duplicateTemplatePart + : undefined, canCreate && postTypeConfig.slug === 'wp_block' ? duplicatePattern : undefined, @@ -121,7 +124,7 @@ export const registerPostTypeActions = registry.batch( () => { actions.forEach( ( action ) => { - if ( action === undefined ) { + if ( ! action ) { return; } unlock( registry.dispatch( editorStore ) ).registerEntityAction( diff --git a/packages/editor/src/dataviews/types.ts b/packages/editor/src/dataviews/types.ts index e5886792fb9c33..664c2dd417201c 100644 --- a/packages/editor/src/dataviews/types.ts +++ b/packages/editor/src/dataviews/types.ts @@ -34,6 +34,7 @@ export interface BasePost extends CommonPost { featured_media?: number; menu_order?: number; ping_status?: 'open' | 'closed'; + link?: string; } export interface Template extends CommonPost { @@ -72,6 +73,7 @@ export type PostWithPermissions = Post & { export interface PostType { slug: string; + viewable: boolean; supports?: { 'page-attributes'?: boolean; title?: boolean; From 3ec1ced865757f6568c42c126acb3478b25fe2be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9?= <583546+oandregal@users.noreply.github.com> Date: Tue, 13 Aug 2024 13:43:15 +0200 Subject: [PATCH 066/126] Add `status` to quick edit (#64398) Co-authored-by: oandregal Co-authored-by: youknowriad Co-authored-by: ntsekouras --- .../src/components/post-edit/index.js | 29 +++++++++++++- .../src/components/post-fields/index.js | 40 +++++++++++++++---- 2 files changed, 59 insertions(+), 10 deletions(-) diff --git a/packages/edit-site/src/components/post-edit/index.js b/packages/edit-site/src/components/post-edit/index.js index 80304f16503705..0ec63589d97673 100644 --- a/packages/edit-site/src/components/post-edit/index.js +++ b/packages/edit-site/src/components/post-edit/index.js @@ -42,13 +42,38 @@ function PostEditForm( { postType, postId } ) { ); const [ multiEdits, setMultiEdits ] = useState( {} ); const { editEntityRecord } = useDispatch( coreDataStore ); - const { fields } = usePostFields(); + const { fields: _fields } = usePostFields(); + const fields = useMemo( + () => + _fields?.map( ( field ) => { + if ( field.id === 'status' ) { + return { + ...field, + elements: field.elements.filter( + ( element ) => element.value !== 'trash' + ), + }; + } + return field; + } ), + [ _fields ] + ); const form = { type: 'panel', - fields: [ 'title', 'author', 'date', 'comment_status' ], + fields: [ 'title', 'status', 'date', 'author', 'comment_status' ], }; const onChange = ( edits ) => { for ( const id of ids ) { + if ( + edits.status !== 'future' && + record.status === 'future' && + new Date( record.date ) > new Date() + ) { + edits.date = null; + } + if ( edits.status === 'private' && record.password ) { + edits.password = ''; + } editEntityRecord( 'postType', postType, id, edits ); if ( ids.length > 1 ) { setMultiEdits( ( prev ) => ( { diff --git a/packages/edit-site/src/components/post-fields/index.js b/packages/edit-site/src/components/post-fields/index.js index b03b2c6f5be3c4..9e59b23d61922d 100644 --- a/packages/edit-site/src/components/post-fields/index.js +++ b/packages/edit-site/src/components/post-fields/index.js @@ -42,11 +42,36 @@ import Media from '../media'; // See https://github.com/WordPress/gutenberg/issues/55886 // We do not support custom statutes at the moment. const STATUSES = [ - { value: 'draft', label: __( 'Draft' ), icon: drafts }, - { value: 'future', label: __( 'Scheduled' ), icon: scheduled }, - { value: 'pending', label: __( 'Pending Review' ), icon: pending }, - { value: 'private', label: __( 'Private' ), icon: notAllowed }, - { value: 'publish', label: __( 'Published' ), icon: published }, + { + value: 'draft', + label: __( 'Draft' ), + icon: drafts, + description: __( 'Not ready to publish.' ), + }, + { + value: 'future', + label: __( 'Scheduled' ), + icon: scheduled, + description: __( 'Publish automatically on a chosen date.' ), + }, + { + value: 'pending', + label: __( 'Pending Review' ), + icon: pending, + description: __( 'Waiting for review before publishing.' ), + }, + { + value: 'private', + label: __( 'Private' ), + icon: notAllowed, + description: __( 'Only visible to site admins and editors.' ), + }, + { + value: 'publish', + label: __( 'Published' ), + icon: published, + description: __( 'Visible to everyone.' ), + }, { value: 'trash', label: __( 'Trash' ), icon: trash }, ]; @@ -258,11 +283,10 @@ function usePostFields( viewType ) { { label: __( 'Status' ), id: 'status', - getValue: ( { item } ) => - STATUSES.find( ( { value } ) => value === item.status ) - ?.label ?? item.status, + type: 'text', elements: STATUSES, render: PostStatusField, + Edit: 'radio', enableSorting: false, filterBy: { operators: [ OPERATOR_IS_ANY ], From 9ca5628ed1cd11975086b25cd0cc318352b94619 Mon Sep 17 00:00:00 2001 From: JuanMa Date: Tue, 13 Aug 2024 13:33:04 +0100 Subject: [PATCH 067/126] Fix tip link in DataViews docs (#64469) Co-authored-by: juanmaguitar Co-authored-by: oandregal --- packages/dataviews/README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/dataviews/README.md b/packages/dataviews/README.md index 56cecedaef66ca..368880b69b14f0 100644 --- a/packages/dataviews/README.md +++ b/packages/dataviews/README.md @@ -30,8 +30,7 @@ const Example = () => { }; ``` -> [!TIP] -> At https://wordpress.github.io/gutenberg/?path=/docs/dataviews-dataviews--docs there's an example implementation of the Dataviews component." + ## Properties From 02d052033868ea554bbdfee9b18b766e77128a6b Mon Sep 17 00:00:00 2001 From: George Mamadashvili Date: Tue, 13 Aug 2024 17:18:12 +0400 Subject: [PATCH 068/126] Edit Post: Remove user pattern preloading (#64459) * Edit Post: Remove user pattern preloading * Update backport changelog Co-authored-by: Mamaduka Co-authored-by: tyxla --- backport-changelog/6.7/7179.md | 1 + lib/compat/wordpress-6.7/rest-api.php | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/backport-changelog/6.7/7179.md b/backport-changelog/6.7/7179.md index f359b6610a94e6..a6e9cc4efe2cd0 100644 --- a/backport-changelog/6.7/7179.md +++ b/backport-changelog/6.7/7179.md @@ -1,3 +1,4 @@ https://github.com/WordPress/wordpress-develop/pull/7179 * https://github.com/WordPress/gutenberg/pull/64401 +* https://github.com/WordPress/gutenberg/pull/64459 diff --git a/lib/compat/wordpress-6.7/rest-api.php b/lib/compat/wordpress-6.7/rest-api.php index fe2aac9c2580ae..2520a06fbb18be 100644 --- a/lib/compat/wordpress-6.7/rest-api.php +++ b/lib/compat/wordpress-6.7/rest-api.php @@ -26,6 +26,24 @@ function gutenberg_block_editor_preload_paths_6_7( $paths, $context ) { } } + if ( 'core/edit-post' === $context->name ) { + $reusable_blocks_key = array_search( + add_query_arg( + array( + 'context' => 'edit', + 'per_page' => -1, + ), + rest_get_route_for_post_type_items( 'wp_block' ) + ), + $paths, + true + ); + + if ( false !== $parts_key ) { + unset( $paths[ $reusable_blocks_key ] ); + } + } + return $paths; } add_filter( 'block_editor_rest_api_preload_paths', 'gutenberg_block_editor_preload_paths_6_7', 10, 2 ); From cedbe111a2cc2b806356a985df1f47034b7545df Mon Sep 17 00:00:00 2001 From: Nik Tsekouras Date: Tue, 13 Aug 2024 17:55:15 +0300 Subject: [PATCH 069/126] Update the copy of quick edit tooltip (#64475) Co-authored-by: ntsekouras Co-authored-by: youknowriad Co-authored-by: jasmussen --- packages/edit-site/src/components/post-list/index.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/edit-site/src/components/post-list/index.js b/packages/edit-site/src/components/post-list/index.js index 35ecaaa8424fd4..bbfece24518495 100644 --- a/packages/edit-site/src/components/post-list/index.js +++ b/packages/edit-site/src/components/post-list/index.js @@ -366,11 +366,7 @@ export default function PostList( { postType } ) { size="compact" isPressed={ quickEdit } icon={ drawerRight } - label={ - ! quickEdit - ? __( 'Show quick edit sidebar' ) - : __( 'Close quick edit sidebar' ) - } + label={ __( 'Toggle details panel' ) } onClick={ () => { history.push( { ...location.params, From b3ce94ea25990e329ce4c08f3d49eb07e7ec929b Mon Sep 17 00:00:00 2001 From: Marin Atanasov <8436925+tyxla@users.noreply.github.com> Date: Tue, 13 Aug 2024 18:40:08 +0300 Subject: [PATCH 070/126] Edit Post: Fix user pattern preloading filter (#64477) * Edit Post: Fix user pattern preloading filter * Backport changelog Co-authored-by: tyxla Co-authored-by: Mamaduka --- backport-changelog/6.7/7179.md | 1 + lib/compat/wordpress-6.7/rest-api.php | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/backport-changelog/6.7/7179.md b/backport-changelog/6.7/7179.md index a6e9cc4efe2cd0..d777eace2cb05e 100644 --- a/backport-changelog/6.7/7179.md +++ b/backport-changelog/6.7/7179.md @@ -2,3 +2,4 @@ https://github.com/WordPress/wordpress-develop/pull/7179 * https://github.com/WordPress/gutenberg/pull/64401 * https://github.com/WordPress/gutenberg/pull/64459 +* https://github.com/WordPress/gutenberg/pull/64477 diff --git a/lib/compat/wordpress-6.7/rest-api.php b/lib/compat/wordpress-6.7/rest-api.php index 2520a06fbb18be..081c22c8102914 100644 --- a/lib/compat/wordpress-6.7/rest-api.php +++ b/lib/compat/wordpress-6.7/rest-api.php @@ -39,7 +39,7 @@ function gutenberg_block_editor_preload_paths_6_7( $paths, $context ) { true ); - if ( false !== $parts_key ) { + if ( false !== $reusable_blocks_key ) { unset( $paths[ $reusable_blocks_key ] ); } } From 5dae25b15e82a59d28c6c065b5a3745f3f83980f Mon Sep 17 00:00:00 2001 From: Jan Pfeil Date: Tue, 13 Aug 2024 18:40:35 +0200 Subject: [PATCH 071/126] fix typo in block-wrapper.md (#64447) --- docs/getting-started/fundamentals/block-wrapper.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/getting-started/fundamentals/block-wrapper.md b/docs/getting-started/fundamentals/block-wrapper.md index 39c80262d7bcbe..98c435f6ebe2f7 100644 --- a/docs/getting-started/fundamentals/block-wrapper.md +++ b/docs/getting-started/fundamentals/block-wrapper.md @@ -102,7 +102,7 @@ The [example block](https://github.com/WordPress/block-development-examples/tree ## Dynamic render markup -In dynamic blocks, where the font-end markup is rendered server-side, you can utilize the [`get_block_wrapper_attributes()`](https://developer.wordpress.org/reference/functions/get_block_wrapper_attributes/) function to output the necessary classes and attributes just like you would use `useBlockProps.save()` in the `save` function. (See [example](https://github.com/WordPress/block-development-examples/blob/f68640f42d993f0866d1879f67c73910285ca114/plugins/block-dynamic-rendering-64756b/src/render.php#L11)) +In dynamic blocks, where the front-end markup is rendered server-side, you can utilize the [`get_block_wrapper_attributes()`](https://developer.wordpress.org/reference/functions/get_block_wrapper_attributes/) function to output the necessary classes and attributes just like you would use `useBlockProps.save()` in the `save` function. (See [example](https://github.com/WordPress/block-development-examples/blob/f68640f42d993f0866d1879f67c73910285ca114/plugins/block-dynamic-rendering-64756b/src/render.php#L11)) ```php

> From 3be25a1db5915a94fe755f078ed35b63ebf7cc91 Mon Sep 17 00:00:00 2001 From: James Koster Date: Tue, 13 Aug 2024 20:52:40 +0100 Subject: [PATCH 072/126] Apply minimal variant to pagination dropdown (#63815) Co-authored-by: jameskoster Co-authored-by: mirka <0mirka00@git.wordpress.org> Co-authored-by: t-hamano Co-authored-by: ntsekouras Co-authored-by: swissspidy Co-authored-by: jasmussen --- .../components/dataviews-pagination/index.tsx | 51 +++++++++++++------ .../dataviews-pagination/style.scss | 13 +++-- 2 files changed, 44 insertions(+), 20 deletions(-) diff --git a/packages/dataviews/src/components/dataviews-pagination/index.tsx b/packages/dataviews/src/components/dataviews-pagination/index.tsx index f8ebf41469d949..f022b382cdb70d 100644 --- a/packages/dataviews/src/components/dataviews-pagination/index.tsx +++ b/packages/dataviews/src/components/dataviews-pagination/index.tsx @@ -21,10 +21,31 @@ function DataViewsPagination() { onChangeView, paginationInfo: { totalItems = 0, totalPages }, } = useContext( DataViewsContext ); + if ( ! totalItems || ! totalPages ) { return null; } + const currentPage = view.page ?? 1; + const pageSelectOptions = Array.from( Array( totalPages ) ).map( + ( _, i ) => { + const page = i + 1; + return { + value: page.toString(), + label: page.toString(), + 'aria-label': + currentPage === page + ? sprintf( + // translators: Current page number in total number of pages + __( 'Page %1$s of %2$s' ), + currentPage, + totalPages + ) + : page.toString(), + }; + } + ); + return ( !! totalItems && totalPages !== 1 && ( @@ -37,37 +58,35 @@ function DataViewsPagination() { { createInterpolateElement( sprintf( - // translators: %s: Total number of pages. - _x( 'Page of %s', 'paging' ), + // translators: 1: Current page number, 2: Total number of pages. + _x( + '

Page
%1$s
of %2$s
', + 'paging' + ), + '', totalPages ), { - CurrentPageControl: ( + div:
, + CurrentPage: ( { - const page = i + 1; - return { - value: page.toString(), - label: page.toString(), - }; - } ) } + value={ currentPage.toString() } + options={ pageSelectOptions } onChange={ ( newValue ) => { onChangeView( { ...view, page: +newValue, } ); } } - size="compact" + size="small" __nextHasNoMarginBottom + variant="minimal" /> ), } diff --git a/packages/dataviews/src/components/dataviews-pagination/style.scss b/packages/dataviews/src/components/dataviews-pagination/style.scss index 4e754ab90fa54a..16f064cc3a5178 100644 --- a/packages/dataviews/src/components/dataviews-pagination/style.scss +++ b/packages/dataviews/src/components/dataviews-pagination/style.scss @@ -5,17 +5,22 @@ background-color: $white; padding: $grid-unit-15 $grid-unit-60; border-top: $border-width solid $gray-100; - color: $gray-700; flex-shrink: 0; transition: padding ease-out 0.1s; @include reduce-motion("transition"); } -.dataviews-pagination__page-selection { +.dataviews-pagination__page-select { font-size: 11px; - text-transform: uppercase; font-weight: 500; - color: $gray-900; + text-transform: uppercase; + + @include break-small() { + .components-select-control__input { + font-size: 11px !important; + font-weight: 500; + } + } } /* stylelint-disable-next-line scss/at-rule-no-unknown -- '@container' not globally permitted */ From 562fff19e73a1416bdc533ef372b8117beeed436 Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Wed, 14 Aug 2024 04:53:08 +0900 Subject: [PATCH 073/126] Start adding lint rules for 40px default size (#64410) * Start adding lint rules for 40px default size * Make stricter Co-authored-by: mirka <0mirka00@git.wordpress.org> Co-authored-by: tyxla --- .eslintrc.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/.eslintrc.js b/.eslintrc.js index 6143d62c28bc07..0dc184eabb8b04 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -278,7 +278,6 @@ module.exports = { }, }, { - // Temporary rules until we're ready to officially deprecate the bottom margins. files: [ 'packages/*/src/**/*.[tj]s?(x)' ], excludedFiles: [ 'packages/components/src/**/@(test|stories)/**', @@ -289,6 +288,7 @@ module.exports = { 'error', ...restrictedSyntax, ...restrictedSyntaxComponents, + // Temporary rules until we're ready to officially deprecate the bottom margins. ...[ 'BaseControl', 'CheckboxControl', @@ -309,6 +309,19 @@ module.exports = { componentName + ' should have the `__nextHasNoMarginBottom` prop to opt-in to the new margin-free styles.', } ) ), + // Temporary rules until we're ready to officially default to the new size. + ...[ + 'BorderBoxControl', + 'BorderControl', + 'DimensionControl', + 'FontSizePicker', + ].map( ( componentName ) => ( { + // Falsy `__next40pxDefaultSize` without a non-default `size` prop. + selector: `JSXOpeningElement[name.name="${ componentName }"]:not(:has(JSXAttribute[name.name="__next40pxDefaultSize"][value.expression.value!=false])):not(:has(JSXAttribute[name.name="size"][value.value!="default"]))`, + message: + componentName + + ' should have the `__next40pxDefaultSize` prop to opt-in to the new default size.', + } ) ), ], }, }, From 40b51b8619e120c96c3dfc6844ed72f3c84e2ca4 Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Wed, 14 Aug 2024 05:28:37 +0900 Subject: [PATCH 074/126] QueryControls: Default to new 40px size (#64457) * QueryControls: Default to new 40px size * Add changelog Co-authored-by: mirka <0mirka00@git.wordpress.org> Co-authored-by: tyxla --- packages/components/CHANGELOG.md | 1 + packages/components/src/query-controls/index.tsx | 11 +++++------ packages/components/src/query-controls/types.ts | 3 ++- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 761e6604a127a2..6b95e23818c864 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -11,6 +11,7 @@ ### Enhancements - `Composite`: improve Storybook examples and add interactive controls ([#64397](https://github.com/WordPress/gutenberg/pull/64397)). +- `QueryControls`: Default to new 40px size ([#64457](https://github.com/WordPress/gutenberg/pull/64457)). - `TimePicker`: add `hideLabelFromVision` prop ([#64267](https://github.com/WordPress/gutenberg/pull/64267)). - `DropdownMenuV2`: adopt elevation scale ([#64432](https://github.com/WordPress/gutenberg/pull/64432)). diff --git a/packages/components/src/query-controls/index.tsx b/packages/components/src/query-controls/index.tsx index 3557335ebac5a0..452dd303c778bb 100644 --- a/packages/components/src/query-controls/index.tsx +++ b/packages/components/src/query-controls/index.tsx @@ -60,7 +60,6 @@ function isMultipleCategorySelection( * ``` */ export function QueryControls( { - __next40pxDefaultSize = false, authorList, selectedAuthorId, numberOfItems, @@ -82,7 +81,7 @@ export function QueryControls( { onOrderChange && onOrderByChange && ( Date: Wed, 14 Aug 2024 06:32:10 +0900 Subject: [PATCH 075/126] TextControl: Add lint rule for 40px size prop usage (#64455) * TextControl: Add lint rule for 40px size prop usage * Fixup * Fixup formatting Co-authored-by: mirka <0mirka00@git.wordpress.org> Co-authored-by: tyxla --- .eslintrc.js | 8 ++++++++ .../src/components/date-format-picker/index.js | 2 ++ .../src/components/media-upload/index.native.js | 2 ++ .../src/components/url-popover/image-url-input-ui.js | 4 ++++ packages/block-library/src/button/edit.js | 2 ++ packages/block-library/src/form-input/edit.js | 2 ++ packages/block-library/src/form/edit.js | 2 ++ packages/block-library/src/image/image.js | 2 ++ .../src/navigation/edit/navigation-menu-name-control.js | 2 ++ packages/block-library/src/post-comment/edit.js | 2 ++ packages/block-library/src/post-featured-image/edit.js | 2 ++ packages/block-library/src/post-terms/edit.js | 2 ++ packages/block-library/src/post-title/edit.js | 2 ++ packages/block-library/src/social-link/edit.js | 4 ++++ .../src/template-part/edit/advanced-controls.js | 2 ++ packages/block-library/src/video/tracks-editor.js | 4 ++++ .../src/components/sidebar-dataviews/add-new-view.js | 2 ++ .../components/sidebar-dataviews/custom-dataviews-list.js | 2 ++ .../src/components/post-publish-panel/postpublish.js | 2 ++ packages/editor/src/components/post-slug/index.js | 2 ++ .../post-taxonomies/hierarchical-term-selector.js | 2 ++ .../reusable-block-convert-button.js | 2 ++ 22 files changed, 56 insertions(+) diff --git a/.eslintrc.js b/.eslintrc.js index 0dc184eabb8b04..eb2d2db47e4cab 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -322,6 +322,14 @@ module.exports = { componentName + ' should have the `__next40pxDefaultSize` prop to opt-in to the new default size.', } ) ), + // Temporary rules until all existing components have the `__next40pxDefaultSize` prop. + ...[ 'TextControl' ].map( ( componentName ) => ( { + // Not strict. Allows pre-existing __next40pxDefaultSize={ false } usage until they are all manually updated. + selector: `JSXOpeningElement[name.name="${ componentName }"]:not(:has(JSXAttribute[name.name="__next40pxDefaultSize"])):not(:has(JSXAttribute[name.name="size"]))`, + message: + componentName + + ' should have the `__next40pxDefaultSize` prop to opt-in to the new default size.', + } ) ), ], }, }, diff --git a/packages/block-editor/src/components/date-format-picker/index.js b/packages/block-editor/src/components/date-format-picker/index.js index 15beec4ac6ed54..63c977b111e01c 100644 --- a/packages/block-editor/src/components/date-format-picker/index.js +++ b/packages/block-editor/src/components/date-format-picker/index.js @@ -149,6 +149,8 @@ function NonDefaultControls( { format, onChange } ) { /> { isCustom && ( { isLinkTag && ( { ) } /> ( diff --git a/packages/block-library/src/post-featured-image/edit.js b/packages/block-library/src/post-featured-image/edit.js index c9f4645e5e8654..675580c71c1d7b 100644 --- a/packages/block-library/src/post-featured-image/edit.js +++ b/packages/block-library/src/post-featured-image/edit.js @@ -230,6 +230,8 @@ export default function PostFeaturedImageEdit( { checked={ linkTarget === '_blank' } /> onChange( { diff --git a/packages/edit-site/src/components/sidebar-dataviews/add-new-view.js b/packages/edit-site/src/components/sidebar-dataviews/add-new-view.js index aabb49c14a2ff7..69cca49fd84563 100644 --- a/packages/edit-site/src/components/sidebar-dataviews/add-new-view.js +++ b/packages/edit-site/src/components/sidebar-dataviews/add-new-view.js @@ -78,6 +78,8 @@ function AddNewItemModalContent( { type, setIsAdding } ) { >
{ showFilter && ( Date: Wed, 14 Aug 2024 07:15:24 +0900 Subject: [PATCH 076/126] Deprecate bottom margin on BaseControl-based components (#64408) * BaseControl: Deprecate bottom margin * Propagate to components * Missed spots * Add changelog * List all affected components in changelog Co-authored-by: mirka <0mirka00@git.wordpress.org> Co-authored-by: tyxla --- packages/components/CHANGELOG.md | 17 ++++++++++ .../components/src/base-control/index.tsx | 13 +++++++ packages/components/src/base-control/types.ts | 7 ++++ .../components/src/checkbox-control/index.tsx | 1 + .../checkbox-control/stories/index.story.tsx | 1 + .../src/checkbox-control/test/index.tsx | 9 ++++- .../components/src/combobox-control/index.tsx | 1 + .../combobox-control/stories/index.story.tsx | 7 ++-- .../src/combobox-control/test/index.tsx | 6 +++- .../src/dimension-control/index.tsx | 34 +++++++++++++------ .../dimension-control/stories/index.story.tsx | 1 + .../test/__snapshots__/index.test.js.snap | 16 --------- .../src/dimension-control/test/index.test.js | 6 +++- .../src/focal-point-picker/index.tsx | 1 + .../stories/index.story.tsx | 3 ++ .../src/focal-point-picker/test/index.tsx | 6 +++- .../components/src/range-control/index.tsx | 1 + .../src/range-control/stories/index.story.tsx | 7 ++++ .../src/range-control/test/index.tsx | 8 ++++- .../components/src/search-control/index.tsx | 11 +++--- .../search-control/stories/index.story.tsx | 1 + .../src/search-control/test/index.tsx | 1 + .../components/src/select-control/index.tsx | 1 + .../select-control/stories/index.story.tsx | 13 ++++--- .../select-control/test/select-control.tsx | 18 ++++++---- .../components/src/text-control/index.tsx | 1 + .../src/text-control/stories/index.story.tsx | 4 ++- .../src/text-control/test/text-control.tsx | 6 +++- .../components/src/textarea-control/index.tsx | 1 + .../textarea-control/stories/index.story.tsx | 1 + .../components/src/toggle-control/index.tsx | 9 +++++ .../toggle-control/stories/index.story.tsx | 1 + .../src/toggle-control/test/index.tsx | 8 ++++- .../test/__snapshots__/index.tsx.snap | 16 --------- .../src/toggle-group-control/test/index.tsx | 6 +++- .../toggle-group-control/component.tsx | 1 + packages/components/src/tree-select/index.tsx | 21 +++++++++--- .../src/tree-select/stories/index.story.tsx | 1 + 38 files changed, 191 insertions(+), 75 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 6b95e23818c864..36b1c8fcc55d00 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -2,6 +2,23 @@ ## Unreleased +### Deprecations + +- Deprecate bottom margin on the following `BaseControl`-based components. Set the `__nextHasNoMarginBottom` prop to true to start opting into the new styles, which will become the default in a future version ([#64408](https://github.com/WordPress/gutenberg/pull/64408)). + - `BaseControl` + - `CheckboxControl` + - `ComboboxControl` + - `DimensionControl` + - `FocalPointPicker` + - `RangeControl` + - `SearchControl` + - `SelectControl` + - `TextControl` + - `TextareaControl` + - `ToggleControl` + - `ToggleGroupControl` + - `TreeSelect` + ### New Features - `Composite`: add stable version of the component ([#63564](https://github.com/WordPress/gutenberg/pull/63564)). diff --git a/packages/components/src/base-control/index.tsx b/packages/components/src/base-control/index.tsx index 77899b6480daed..423636a92cd5f0 100644 --- a/packages/components/src/base-control/index.tsx +++ b/packages/components/src/base-control/index.tsx @@ -7,6 +7,7 @@ import type { ForwardedRef } from 'react'; /** * WordPress dependencies */ +import deprecated from '@wordpress/deprecated'; import { forwardRef } from '@wordpress/element'; /** @@ -31,6 +32,7 @@ const UnconnectedBaseControl = ( ) => { const { __nextHasNoMarginBottom = false, + __associatedWPComponentName = 'BaseControl', id, label, hideLabelFromVision = false, @@ -39,6 +41,17 @@ const UnconnectedBaseControl = ( children, } = useContextSystem( props, 'BaseControl' ); + if ( ! __nextHasNoMarginBottom ) { + deprecated( + `Bottom margin styles for wp.components.${ __associatedWPComponentName }`, + { + since: '6.7', + version: '7.0', + hint: 'Set the `__nextHasNoMarginBottom` prop to true to start opting into the new styles, which will become the default in a future version.', + } + ); + } + return ( = DefaultTemplate.bind( {} ); Default.args = { + __nextHasNoMarginBottom: true, label: 'Is author', help: 'Is the user an author or not?', }; diff --git a/packages/components/src/checkbox-control/test/index.tsx b/packages/components/src/checkbox-control/test/index.tsx index 899a9b100015b3..547f479184e862 100644 --- a/packages/components/src/checkbox-control/test/index.tsx +++ b/packages/components/src/checkbox-control/test/index.tsx @@ -20,13 +20,20 @@ const noop = () => {}; const getInput = () => screen.getByRole( 'checkbox' ) as HTMLInputElement; const CheckboxControl = ( props: Omit< CheckboxControlProps, 'onChange' > ) => { - return ; + return ( + + ); }; const ControlledCheckboxControl = ( { onChange }: CheckboxControlProps ) => { const [ isChecked, setChecked ] = useState( false ); return ( { setChecked( value ); diff --git a/packages/components/src/combobox-control/index.tsx b/packages/components/src/combobox-control/index.tsx index e3b1434be8c7c2..fc3ecccf0b6599 100644 --- a/packages/components/src/combobox-control/index.tsx +++ b/packages/components/src/combobox-control/index.tsx @@ -320,6 +320,7 @@ function ComboboxControl( props: ComboboxControlProps ) { = ( { }; export const Default = Template.bind( {} ); Default.args = { + __nextHasNoMarginBottom: true, allowReset: false, label: 'Select a country', options: countryOptions, @@ -135,8 +136,7 @@ const optionsWithDisabledOptions = countryOptions.map( ( option, index ) => ( { } ) ); WithDisabledOptions.args = { - allowReset: false, - label: 'Select a country', + ...Default.args, options: optionsWithDisabledOptions, }; @@ -148,8 +148,7 @@ WithDisabledOptions.args = { export const NotExpandOnFocus = Template.bind( {} ); NotExpandOnFocus.args = { - allowReset: false, - label: 'Select a country', + ...Default.args, options: countryOptions, expandOnFocus: false, }; diff --git a/packages/components/src/combobox-control/test/index.tsx b/packages/components/src/combobox-control/test/index.tsx index 76ce9cc4724c54..adc76590c24538 100644 --- a/packages/components/src/combobox-control/test/index.tsx +++ b/packages/components/src/combobox-control/test/index.tsx @@ -12,7 +12,7 @@ import { useState } from '@wordpress/element'; /** * Internal dependencies */ -import ComboboxControl from '..'; +import _ComboboxControl from '..'; import type { ComboboxControlOption, ComboboxControlProps } from '../types'; const timezones = [ @@ -57,6 +57,10 @@ const getAllOptions = () => screen.getAllByRole( 'option' ); const getOptionSearchString = ( option: ComboboxControlOption ) => option.label.substring( 0, 11 ); +const ComboboxControl = ( props: ComboboxControlProps ) => { + return <_ComboboxControl { ...props } __nextHasNoMarginBottom />; +}; + const ControlledComboboxControl = ( { value: valueProp, onChange, diff --git a/packages/components/src/dimension-control/index.tsx b/packages/components/src/dimension-control/index.tsx index 114ebe987dd35d..52662f31c3f24c 100644 --- a/packages/components/src/dimension-control/index.tsx +++ b/packages/components/src/dimension-control/index.tsx @@ -16,6 +16,15 @@ import SelectControl from '../select-control'; import sizesTable, { findSizeBySlug } from './sizes'; import type { DimensionControlProps, Size } from './types'; import type { SelectControlSingleSelectionProps } from '../select-control/types'; +import { ContextSystemProvider } from '../context'; + +const CONTEXT_VALUE = { + BaseControl: { + // Temporary during deprecation grace period: Overrides the underlying `__associatedWPComponentName` + // via the context system to override the value set by SelectControl. + _overrides: { __associatedWPComponentName: 'DimensionControl' }, + }, +}; /** * `DimensionControl` is a component designed to provide a UI to control spacing and/or dimensions. @@ -87,16 +96,21 @@ export function DimensionControl( props: DimensionControlProps ) { ); return ( - + + + ); } diff --git a/packages/components/src/dimension-control/stories/index.story.tsx b/packages/components/src/dimension-control/stories/index.story.tsx index 33d5bad4ff4b19..3a6da44f461164 100644 --- a/packages/components/src/dimension-control/stories/index.story.tsx +++ b/packages/components/src/dimension-control/stories/index.story.tsx @@ -44,6 +44,7 @@ const Template: StoryFn< typeof DimensionControl > = ( args ) => ( export const Default = Template.bind( {} ); Default.args = { + __nextHasNoMarginBottom: true, label: 'Please select a size', sizes, }; diff --git a/packages/components/src/dimension-control/test/__snapshots__/index.test.js.snap b/packages/components/src/dimension-control/test/__snapshots__/index.test.js.snap index 5990fbbd4a3f5f..658fe7febc02bc 100644 --- a/packages/components/src/dimension-control/test/__snapshots__/index.test.js.snap +++ b/packages/components/src/dimension-control/test/__snapshots__/index.test.js.snap @@ -13,10 +13,6 @@ exports[`DimensionControl rendering renders with custom sizes 1`] = ` box-sizing: inherit; } -.emotion-2 { - margin-bottom: calc(4px * 2); -} - .components-panel__row .emotion-2 { margin-bottom: inherit; } @@ -299,10 +295,6 @@ exports[`DimensionControl rendering renders with defaults 1`] = ` box-sizing: inherit; } -.emotion-2 { - margin-bottom: calc(4px * 2); -} - .components-panel__row .emotion-2 { margin-bottom: inherit; } @@ -595,10 +587,6 @@ exports[`DimensionControl rendering renders with icon and custom icon label 1`] box-sizing: inherit; } -.emotion-2 { - margin-bottom: calc(4px * 2); -} - .components-panel__row .emotion-2 { margin-bottom: inherit; } @@ -903,10 +891,6 @@ exports[`DimensionControl rendering renders with icon and default icon label 1`] box-sizing: inherit; } -.emotion-2 { - margin-bottom: calc(4px * 2); -} - .components-panel__row .emotion-2 { margin-bottom: inherit; } diff --git a/packages/components/src/dimension-control/test/index.test.js b/packages/components/src/dimension-control/test/index.test.js index 1d65dd86c7e7c8..1b34d2983ad0f1 100644 --- a/packages/components/src/dimension-control/test/index.test.js +++ b/packages/components/src/dimension-control/test/index.test.js @@ -12,7 +12,11 @@ import { plus } from '@wordpress/icons'; /** * Internal dependencies */ -import { DimensionControl } from '../'; +import { DimensionControl as _DimensionControl } from '../'; + +const DimensionControl = ( props ) => { + return <_DimensionControl { ...props } __nextHasNoMarginBottom />; +}; describe( 'DimensionControl', () => { const onChangeHandler = jest.fn(); diff --git a/packages/components/src/focal-point-picker/index.tsx b/packages/components/src/focal-point-picker/index.tsx index 4575108fe6a21d..8f299751be0d48 100644 --- a/packages/components/src/focal-point-picker/index.tsx +++ b/packages/components/src/focal-point-picker/index.tsx @@ -251,6 +251,7 @@ export function FocalPointPicker( { = ( { }; export const Default = Template.bind( {} ); +Default.args = { + __nextHasNoMarginBottom: true, +}; export const Image = Template.bind( {} ); Image.args = { diff --git a/packages/components/src/focal-point-picker/test/index.tsx b/packages/components/src/focal-point-picker/test/index.tsx index 1eccced32c70af..377ba6c4e9e6b3 100644 --- a/packages/components/src/focal-point-picker/test/index.tsx +++ b/packages/components/src/focal-point-picker/test/index.tsx @@ -7,12 +7,16 @@ import userEvent from '@testing-library/user-event'; /** * Internal dependencies */ -import Picker from '..'; +import _Picker from '..'; import type { FocalPointPickerProps } from '../types'; type Log = { name: string; args: unknown[] }; type EventLogger = ( name: string, args: unknown[] ) => void; +const Picker = ( props: React.ComponentProps< typeof _Picker > ) => { + return <_Picker { ...props } __nextHasNoMarginBottom />; +}; + const props: FocalPointPickerProps = { onChange: jest.fn(), url: 'test-url', diff --git a/packages/components/src/range-control/index.tsx b/packages/components/src/range-control/index.tsx index 5b4ecfa585679b..20d9712df9c251 100644 --- a/packages/components/src/range-control/index.tsx +++ b/packages/components/src/range-control/index.tsx @@ -213,6 +213,7 @@ function UnforwardedRangeControl( return ( = ( { onChange, ...args } ) => { export const Default: StoryFn< typeof RangeControl > = Template.bind( {} ); Default.args = { + __nextHasNoMarginBottom: true, help: 'Please select how transparent you would like this.', initialPosition: 50, label: 'Opacity', @@ -104,6 +105,7 @@ export const WithAnyStep: StoryFn< typeof RangeControl > = ( { ); }; WithAnyStep.args = { + __nextHasNoMarginBottom: true, label: 'Brightness', step: 'any', }; @@ -167,6 +169,7 @@ export const WithIntegerStepAndMarks: StoryFn< typeof RangeControl > = MarkTemplate.bind( {} ); WithIntegerStepAndMarks.args = { + __nextHasNoMarginBottom: true, label: 'Integer Step', marks: marksBase, max: 10, @@ -183,6 +186,7 @@ export const WithDecimalStepAndMarks: StoryFn< typeof RangeControl > = MarkTemplate.bind( {} ); WithDecimalStepAndMarks.args = { + __nextHasNoMarginBottom: true, marks: [ ...marksBase, { value: 3.5, label: '3.5' }, @@ -202,6 +206,7 @@ export const WithNegativeMinimumAndMarks: StoryFn< typeof RangeControl > = MarkTemplate.bind( {} ); WithNegativeMinimumAndMarks.args = { + __nextHasNoMarginBottom: true, marks: marksWithNegatives, max: 10, min: -10, @@ -217,6 +222,7 @@ export const WithNegativeRangeAndMarks: StoryFn< typeof RangeControl > = MarkTemplate.bind( {} ); WithNegativeRangeAndMarks.args = { + __nextHasNoMarginBottom: true, marks: marksWithNegatives, max: -1, min: -10, @@ -232,6 +238,7 @@ export const WithAnyStepAndMarks: StoryFn< typeof RangeControl > = MarkTemplate.bind( {} ); WithAnyStepAndMarks.args = { + __nextHasNoMarginBottom: true, marks: marksBase, max: 10, min: 0, diff --git a/packages/components/src/range-control/test/index.tsx b/packages/components/src/range-control/test/index.tsx index d843b615ed0078..a4c5d8c6f2bc7f 100644 --- a/packages/components/src/range-control/test/index.tsx +++ b/packages/components/src/range-control/test/index.tsx @@ -6,7 +6,7 @@ import { act, fireEvent, render, screen } from '@testing-library/react'; /** * Internal dependencies */ -import RangeControl from '../'; +import _RangeControl from '../'; const getRangeInput = (): HTMLInputElement => screen.getByRole( 'slider' ); const getNumberInput = (): HTMLInputElement => screen.getByRole( 'spinbutton' ); @@ -15,6 +15,12 @@ const getResetButton = (): HTMLButtonElement => screen.getByRole( 'button' ); const fireChangeEvent = ( input: HTMLInputElement, value?: number | string ) => fireEvent.change( input, { target: { value } } ); +const RangeControl = ( + props: React.ComponentProps< typeof _RangeControl > +) => { + return <_RangeControl { ...props } __nextHasNoMarginBottom />; +}; + describe( 'RangeControl', () => { describe( '#render()', () => { it( 'should trigger change callback with numeric value', () => { diff --git a/packages/components/src/search-control/index.tsx b/packages/components/src/search-control/index.tsx index 08cb3b065c904e..aac905e137e025 100644 --- a/packages/components/src/search-control/index.tsx +++ b/packages/components/src/search-control/index.tsx @@ -77,10 +77,13 @@ function UnforwardedSearchControl( const contextValue = useMemo( () => ( { - // Overrides the underlying BaseControl `__nextHasNoMarginBottom` via the context system - // to provide backwards compatibile margin for SearchControl. - // (In a standard InputControl, the BaseControl `__nextHasNoMarginBottom` is always set to true.) - BaseControl: { _overrides: { __nextHasNoMarginBottom } }, + BaseControl: { + // Overrides the underlying BaseControl `__nextHasNoMarginBottom` via the context system + // to provide backwards compatibile margin for SearchControl. + // (In a standard InputControl, the BaseControl `__nextHasNoMarginBottom` is always set to true.) + _overrides: { __nextHasNoMarginBottom }, + __associatedWPComponentName: 'SearchControl', + }, // `isBorderless` is still experimental and not a public prop for InputControl yet. InputBase: { isBorderless: true }, } ), diff --git a/packages/components/src/search-control/stories/index.story.tsx b/packages/components/src/search-control/stories/index.story.tsx index 433d3eef655adf..215288bb67c9b6 100644 --- a/packages/components/src/search-control/stories/index.story.tsx +++ b/packages/components/src/search-control/stories/index.story.tsx @@ -48,6 +48,7 @@ const Template: StoryFn< typeof SearchControl > = ( { export const Default = Template.bind( {} ); Default.args = { help: 'Help text to explain the input.', + __nextHasNoMarginBottom: true, }; /** diff --git a/packages/components/src/search-control/test/index.tsx b/packages/components/src/search-control/test/index.tsx index f130cab1b2a7cd..c6637945adcf63 100644 --- a/packages/components/src/search-control/test/index.tsx +++ b/packages/components/src/search-control/test/index.tsx @@ -23,6 +23,7 @@ function ControlledSearchControl( { return ( { setValue( ...args ); diff --git a/packages/components/src/select-control/index.tsx b/packages/components/src/select-control/index.tsx index ca9966fc675b86..3686661b8a58dc 100644 --- a/packages/components/src/select-control/index.tsx +++ b/packages/components/src/select-control/index.tsx @@ -99,6 +99,7 @@ function UnforwardedSelectControl< V extends string >( help={ help } id={ id } __nextHasNoMarginBottom={ __nextHasNoMarginBottom } + __associatedWPComponentName="SelectControl" > = ( props ) => { export const Default = SelectControlWithState.bind( {} ); Default.args = { + __nextHasNoMarginBottom: true, options: [ { value: '', label: 'Select an Option', disabled: true }, { value: 'a', label: 'Option A' }, @@ -82,9 +83,11 @@ WithLabelAndHelpText.args = { * As an alternative to the `options` prop, `optgroup`s and `options` can be * passed in as `children` for more customizeability. */ -export const WithCustomChildren: StoryFn< typeof SelectControl > = ( args ) => { - return ( - +export const WithCustomChildren = SelectControlWithState.bind( {} ); +WithCustomChildren.args = { + __nextHasNoMarginBottom: true, + children: ( + <> - - ); + + ), }; export const Minimal = SelectControlWithState.bind( {} ); diff --git a/packages/components/src/select-control/test/select-control.tsx b/packages/components/src/select-control/test/select-control.tsx index 0e8a6891087043..47b684cd20e280 100644 --- a/packages/components/src/select-control/test/select-control.tsx +++ b/packages/components/src/select-control/test/select-control.tsx @@ -7,7 +7,13 @@ import userEvent from '@testing-library/user-event'; /** * Internal dependencies */ -import SelectControl from '..'; +import _SelectControl from '..'; + +const SelectControl = ( + props: React.ComponentProps< typeof _SelectControl > +) => { + return <_SelectControl { ...props } __nextHasNoMarginBottom />; +}; describe( 'SelectControl', () => { it( 'should not render when no options or children are provided', () => { @@ -123,7 +129,7 @@ describe( 'SelectControl', () => { onChange={ onChange } />; - { } ); it( 'should accept an explicit type argument', () => { - + <_SelectControl< 'narrow' | 'value' > // @ts-expect-error "string" is not "narrow" or "value" value="string" options={ [ @@ -166,7 +172,7 @@ describe( 'SelectControl', () => { value: ( 'foo' | 'bar' )[] ) => void = () => {}; - { onChange={ onChange } />; - { } ); it( 'should accept an explicit type argument', () => { - + <_SelectControl< 'narrow' | 'value' > multiple // @ts-expect-error "string" is not "narrow" or "value" value={ [ 'string' ] } diff --git a/packages/components/src/text-control/index.tsx b/packages/components/src/text-control/index.tsx index 1643c5bc37c347..ea2d2c17bb9cf6 100644 --- a/packages/components/src/text-control/index.tsx +++ b/packages/components/src/text-control/index.tsx @@ -41,6 +41,7 @@ function UnforwardedTextControl( return ( = ( { export const Default: StoryFn< typeof TextControl > = DefaultTemplate.bind( {} ); -Default.args = {}; +Default.args = { + __nextHasNoMarginBottom: true, +}; export const WithLabelAndHelpText: StoryFn< typeof TextControl > = DefaultTemplate.bind( {} ); diff --git a/packages/components/src/text-control/test/text-control.tsx b/packages/components/src/text-control/test/text-control.tsx index fc048b93992f08..19b17cae443614 100644 --- a/packages/components/src/text-control/test/text-control.tsx +++ b/packages/components/src/text-control/test/text-control.tsx @@ -6,7 +6,11 @@ import { render, screen } from '@testing-library/react'; /** * Internal dependencies */ -import TextControl from '..'; +import _TextControl from '..'; + +const TextControl = ( props: React.ComponentProps< typeof _TextControl > ) => { + return <_TextControl { ...props } __nextHasNoMarginBottom />; +}; const noop = () => {}; diff --git a/packages/components/src/textarea-control/index.tsx b/packages/components/src/textarea-control/index.tsx index 3b96e11b0621b5..e7528510667b75 100644 --- a/packages/components/src/textarea-control/index.tsx +++ b/packages/components/src/textarea-control/index.tsx @@ -35,6 +35,7 @@ function UnforwardedTextareaControl( return ( = ( { export const Default: StoryFn< typeof TextareaControl > = Template.bind( {} ); Default.args = { + __nextHasNoMarginBottom: true, label: 'Text', help: 'Enter some text', }; diff --git a/packages/components/src/toggle-control/index.tsx b/packages/components/src/toggle-control/index.tsx index 5c64d57d3d0249..d2ee234a9695f8 100644 --- a/packages/components/src/toggle-control/index.tsx +++ b/packages/components/src/toggle-control/index.tsx @@ -10,6 +10,7 @@ import clsx from 'clsx'; */ import { forwardRef } from '@wordpress/element'; import { useInstanceId } from '@wordpress/compose'; +import deprecated from '@wordpress/deprecated'; /** * Internal dependencies @@ -48,6 +49,14 @@ function UnforwardedToggleControl( ! __nextHasNoMarginBottom && css( { marginBottom: space( 3 ) } ) ); + if ( ! __nextHasNoMarginBottom ) { + deprecated( 'Bottom margin styles for wp.components.ToggleControl', { + since: '6.7', + version: '7.0', + hint: 'Set the `__nextHasNoMarginBottom` prop to true to start opting into the new styles, which will become the default in a future version.', + } ); + } + let describedBy, helpLabel; if ( help ) { if ( typeof help === 'function' ) { diff --git a/packages/components/src/toggle-control/stories/index.story.tsx b/packages/components/src/toggle-control/stories/index.story.tsx index b8043b8f48e523..97723aa207a394 100644 --- a/packages/components/src/toggle-control/stories/index.story.tsx +++ b/packages/components/src/toggle-control/stories/index.story.tsx @@ -48,6 +48,7 @@ const Template: StoryFn< typeof ToggleControl > = ( { export const Default = Template.bind( {} ); Default.args = { + __nextHasNoMarginBottom: true, label: 'Enable something', }; diff --git a/packages/components/src/toggle-control/test/index.tsx b/packages/components/src/toggle-control/test/index.tsx index cc89031d9affa3..b0eec2aca6663d 100644 --- a/packages/components/src/toggle-control/test/index.tsx +++ b/packages/components/src/toggle-control/test/index.tsx @@ -6,7 +6,13 @@ import { render, screen } from '@testing-library/react'; /** * Internal dependencies */ -import ToggleControl from '..'; +import _ToggleControl from '..'; + +const ToggleControl = ( + props: React.ComponentProps< typeof _ToggleControl > +) => { + return <_ToggleControl { ...props } __nextHasNoMarginBottom />; +}; describe( 'ToggleControl', () => { it( 'should label the toggle', () => { diff --git a/packages/components/src/toggle-group-control/test/__snapshots__/index.tsx.snap b/packages/components/src/toggle-group-control/test/__snapshots__/index.tsx.snap index 81afc7ac67b05f..d055ea5fcc9838 100644 --- a/packages/components/src/toggle-group-control/test/__snapshots__/index.tsx.snap +++ b/packages/components/src/toggle-group-control/test/__snapshots__/index.tsx.snap @@ -13,10 +13,6 @@ exports[`ToggleGroupControl controlled should render correctly with icons 1`] = box-sizing: inherit; } -.emotion-2 { - margin-bottom: calc(4px * 2); -} - .components-panel__row .emotion-2 { margin-bottom: inherit; } @@ -349,10 +345,6 @@ exports[`ToggleGroupControl controlled should render correctly with text options box-sizing: inherit; } -.emotion-2 { - margin-bottom: calc(4px * 2); -} - .components-panel__row .emotion-2 { margin-bottom: inherit; } @@ -573,10 +565,6 @@ exports[`ToggleGroupControl uncontrolled should render correctly with icons 1`] box-sizing: inherit; } -.emotion-2 { - margin-bottom: calc(4px * 2); -} - .components-panel__row .emotion-2 { margin-bottom: inherit; } @@ -903,10 +891,6 @@ exports[`ToggleGroupControl uncontrolled should render correctly with text optio box-sizing: inherit; } -.emotion-2 { - margin-bottom: calc(4px * 2); -} - .components-panel__row .emotion-2 { margin-bottom: inherit; } diff --git a/packages/components/src/toggle-group-control/test/index.tsx b/packages/components/src/toggle-group-control/test/index.tsx index 661bbb9fc37bab..170db01ae523c2 100644 --- a/packages/components/src/toggle-group-control/test/index.tsx +++ b/packages/components/src/toggle-group-control/test/index.tsx @@ -15,7 +15,7 @@ import { formatLowercase, formatUppercase } from '@wordpress/icons'; */ import Button from '../../button'; import { - ToggleGroupControl, + ToggleGroupControl as _ToggleGroupControl, ToggleGroupControlOption, ToggleGroupControlOptionIcon, } from '../index'; @@ -27,6 +27,10 @@ const hoverOutside = async () => { await hover( document.body, { clientX: 10, clientY: 10 } ); }; +const ToggleGroupControl = ( props: ToggleGroupControlProps ) => { + return <_ToggleGroupControl { ...props } __nextHasNoMarginBottom />; +}; + const ControlledToggleGroupControl = ( { value: valueProp, onChange, diff --git a/packages/components/src/toggle-group-control/toggle-group-control/component.tsx b/packages/components/src/toggle-group-control/toggle-group-control/component.tsx index 8138b76505fe50..1c86c93548f6df 100644 --- a/packages/components/src/toggle-group-control/toggle-group-control/component.tsx +++ b/packages/components/src/toggle-group-control/toggle-group-control/component.tsx @@ -72,6 +72,7 @@ function UnconnectedToggleGroupControl( { ! hideLabelFromVision && ( diff --git a/packages/components/src/tree-select/index.tsx b/packages/components/src/tree-select/index.tsx index 599dee4402ec72..bd92807bff4cc9 100644 --- a/packages/components/src/tree-select/index.tsx +++ b/packages/components/src/tree-select/index.tsx @@ -10,6 +10,15 @@ import { decodeEntities } from '@wordpress/html-entities'; import { SelectControl } from '../select-control'; import type { TreeSelectProps, Tree, Truthy } from './types'; import { useDeprecated36pxDefaultSizeProp } from '../utils/use-deprecated-props'; +import { ContextSystemProvider } from '../context'; + +const CONTEXT_VALUE = { + BaseControl: { + // Temporary during deprecation grace period: Overrides the underlying `__associatedWPComponentName` + // via the context system to override the value set by SelectControl. + _overrides: { __associatedWPComponentName: 'TreeSelect' }, + }, +}; function getSelectOptions( tree: Tree[], @@ -91,11 +100,13 @@ export function TreeSelect( props: TreeSelectProps ) { }, [ noOptionLabel, tree ] ); return ( - + + + ); } diff --git a/packages/components/src/tree-select/stories/index.story.tsx b/packages/components/src/tree-select/stories/index.story.tsx index 0a4212dc791227..33103786bbc541 100644 --- a/packages/components/src/tree-select/stories/index.story.tsx +++ b/packages/components/src/tree-select/stories/index.story.tsx @@ -48,6 +48,7 @@ const TreeSelectWithState: StoryFn< typeof TreeSelect > = ( props ) => { export const Default = TreeSelectWithState.bind( {} ); Default.args = { + __nextHasNoMarginBottom: true, label: 'Label Text', noOptionLabel: 'No parent page', help: 'Help text to explain the select control.', From e0760706272e4d6af3f1b0c7f90ab4ea9e305256 Mon Sep 17 00:00:00 2001 From: Amit Raj <77401999+amitraj2203@users.noreply.github.com> Date: Wed, 14 Aug 2024 11:16:04 +0530 Subject: [PATCH 077/126] chore: Add label prop to SizeControl component (#64428) Co-authored-by: amitraj2203 Co-authored-by: mirka <0mirka00@git.wordpress.org> --- .../src/components/global-styles/size-control/index.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/edit-site/src/components/global-styles/size-control/index.js b/packages/edit-site/src/components/global-styles/size-control/index.js index 28fa64c643cbbd..06ea0bb5617e31 100644 --- a/packages/edit-site/src/components/global-styles/size-control/index.js +++ b/packages/edit-site/src/components/global-styles/size-control/index.js @@ -1,7 +1,6 @@ /** * WordPress dependencies */ -import { __ } from '@wordpress/i18n'; /** * Internal dependencies @@ -26,7 +25,7 @@ function SizeControl( { ...props } ) { const { baseControlProps } = useBaseControlProps( props ); - const { value, onChange, fallbackValue, disabled } = props; + const { value, onChange, fallbackValue, disabled, label } = props; const units = useCustomUnits( { availableUnits: DEFAULT_UNITS, @@ -55,7 +54,7 @@ function SizeControl( { Date: Wed, 14 Aug 2024 08:23:24 +0100 Subject: [PATCH 078/126] Zoon Out: Fix scale calculations (#64478) --- packages/block-editor/src/components/iframe/index.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/block-editor/src/components/iframe/index.js b/packages/block-editor/src/components/iframe/index.js index e7af77920ea127..3b0bce6d56b403 100644 --- a/packages/block-editor/src/components/iframe/index.js +++ b/packages/block-editor/src/components/iframe/index.js @@ -242,8 +242,10 @@ function Iframe( { const isZoomedOut = scale !== 1; useEffect( () => { - prevContainerWidth.current = containerWidth; - }, [ containerWidth ] ); + if ( ! isZoomedOut ) { + prevContainerWidth.current = containerWidth; + } + }, [ containerWidth, isZoomedOut ] ); const disabledRef = useDisabled( { isDisabled: ! readonly } ); const bodyRef = useMergeRefs( [ From c20273736e30e2bfc54f82faaeaad4912b25cd29 Mon Sep 17 00:00:00 2001 From: Gutenberg Repository Automation Date: Wed, 14 Aug 2024 12:18:10 +0000 Subject: [PATCH 079/126] Bump plugin version to 19.0.0 --- gutenberg.php | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gutenberg.php b/gutenberg.php index 6ec0a56f00e74d..66f0aa31a65baa 100644 --- a/gutenberg.php +++ b/gutenberg.php @@ -5,7 +5,7 @@ * Description: Printing since 1440. This is the development plugin for the block editor, site editor, and other future WordPress core functionality. * Requires at least: 6.5 * Requires PHP: 7.2 - * Version: 19.0.0-rc.1 + * Version: 19.0.0 * Author: Gutenberg Team * Text Domain: gutenberg * diff --git a/package-lock.json b/package-lock.json index 471d7b845d24f8..ddfec3a5dddc63 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "gutenberg", - "version": "19.0.0-rc.1", + "version": "19.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "gutenberg", - "version": "19.0.0-rc.1", + "version": "19.0.0", "hasInstallScript": true, "license": "GPL-2.0-or-later", "dependencies": { diff --git a/package.json b/package.json index e4466eb4f470e1..ee78f197a43e28 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "19.0.0-rc.1", + "version": "19.0.0", "private": true, "description": "A new WordPress editor experience.", "author": "The WordPress Contributors", From 54269c00f74805533b5fec9bf14b84d18f8d3ae9 Mon Sep 17 00:00:00 2001 From: Gutenberg Repository Automation Date: Wed, 14 Aug 2024 12:41:28 +0000 Subject: [PATCH 080/126] Update Changelog for 19.0.0 --- changelog.txt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/changelog.txt b/changelog.txt index e85895547e87d1..748df8da3484c7 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,7 +1,6 @@ == Changelog == -= 19.0.0-rc.1 = - += 19.0.0 = ## Changelog @@ -368,6 +367,8 @@ The following contributors merged PRs in this release: @aaronrobertshaw @adamsilverstein @afercia @akasunil @Aljullu @amitraj2203 @andrewserong @carolinan @cbravobernal @Chrico @ciampo @creativecoder @DaniGuardiola @DAreRodz @djcowan @ellatrix @jameskoster @jasmussen @jeryj @jorgefilipecosta @jsnajdr @kebbet @kmanijak @Mamaduka @matiasbenedetto @meteorlxy @mikachan @mirka @mtias @ndiego @noisysocks @oandregal @ramonjd @richtabor @Rishit30G @ryanwelcher @SantosGuillamot @scruffian @shail-mehta @simison @stokesman @t-hamano @talldan @tomdevisser @tomjn @tyxla @up1512001 @wzieba @youknowriad + + = 18.9.0 = ## Changelog From b533bfd6d00c2aa99341568d5328a6ca6f826823 Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Wed, 14 Aug 2024 15:32:14 +0200 Subject: [PATCH 081/126] `Composite`: use internal context to consume composite store (#64493) * Create CompositeContext * Use context to pass store to composite subcomponents * CHANGELOG * Remove double dot * Export `Context`, add basic docs * Update CHANGELOG --- Co-authored-by: ciampo Co-authored-by: tyxla --- packages/components/CHANGELOG.md | 4 +- packages/components/src/composite/README.md | 4 + packages/components/src/composite/context.ts | 14 +++ packages/components/src/composite/index.tsx | 95 ++++++++++++++++++-- packages/components/src/composite/types.ts | 9 ++ 5 files changed, 116 insertions(+), 10 deletions(-) create mode 100644 packages/components/src/composite/context.ts diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 36b1c8fcc55d00..594bb47e187d51 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -23,11 +23,13 @@ - `Composite`: add stable version of the component ([#63564](https://github.com/WordPress/gutenberg/pull/63564)). - `Composite`: add `Hover` and `Typeahead` subcomponents ([#64399](https://github.com/WordPress/gutenberg/pull/64399)). -- `Composite`: export `useCompositeStore, add focus-related props to `Composite`and`Composite.Item` subcomponents ([#64450](https://github.com/WordPress/gutenberg/pull/64450)). +- `Composite`: export `useCompositeStore`, add focus-related props to `Composite`and`Composite.Item` subcomponents ([#64450](https://github.com/WordPress/gutenberg/pull/64450)). +- `Composite`: add `Context` subcomponent ([#64493](https://github.com/WordPress/gutenberg/pull/64493)). ### Enhancements - `Composite`: improve Storybook examples and add interactive controls ([#64397](https://github.com/WordPress/gutenberg/pull/64397)). +- `Composite`: use internal context to forward the composite store to sub-components ([#64493](https://github.com/WordPress/gutenberg/pull/64493)). - `QueryControls`: Default to new 40px size ([#64457](https://github.com/WordPress/gutenberg/pull/64457)). - `TimePicker`: add `hideLabelFromVision` prop ([#64267](https://github.com/WordPress/gutenberg/pull/64267)). - `DropdownMenuV2`: adopt elevation scale ([#64432](https://github.com/WordPress/gutenberg/pull/64432)). diff --git a/packages/components/src/composite/README.md b/packages/components/src/composite/README.md index 3670e31b01e9df..76e345b16d13fa 100644 --- a/packages/components/src/composite/README.md +++ b/packages/components/src/composite/README.md @@ -315,3 +315,7 @@ Allows the component to be rendered as a different HTML element or React compone The contents of the component. - Required: no + +### `Composite.Context` + +The React context used by the composite components. It can be used by to access the composite store, and to forward the context when composite sub-components are rendered across portals (ie. `SlotFill` components) that would not otherwise forward the context to the `Fill` children. diff --git a/packages/components/src/composite/context.ts b/packages/components/src/composite/context.ts new file mode 100644 index 00000000000000..69a052c5bfba19 --- /dev/null +++ b/packages/components/src/composite/context.ts @@ -0,0 +1,14 @@ +/** + * WordPress dependencies + */ +import { createContext, useContext } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import type { CompositeContextProps } from './types'; + +export const CompositeContext = + createContext< CompositeContextProps >( undefined ); + +export const useCompositeContext = () => useContext( CompositeContext ); diff --git a/packages/components/src/composite/index.tsx b/packages/components/src/composite/index.tsx index f5d92330cada3c..0bfcec2bf76600 100644 --- a/packages/components/src/composite/index.tsx +++ b/packages/components/src/composite/index.tsx @@ -16,12 +16,13 @@ import * as Ariakit from '@ariakit/react'; /** * WordPress dependencies */ -import { forwardRef } from '@wordpress/element'; +import { useMemo, forwardRef } from '@wordpress/element'; /** * Internal dependencies */ import type { WordPressComponentProps } from '../context'; +import { CompositeContext, useCompositeContext } from './context'; import type { CompositeStoreProps, CompositeProps, @@ -72,7 +73,14 @@ const Group = forwardRef< HTMLDivElement, WordPressComponentProps< CompositeGroupProps, 'div', false > >( function CompositeGroup( props, ref ) { - return ; + const context = useCompositeContext(); + return ( + + ); } ); Group.displayName = 'Composite.Group'; @@ -80,7 +88,14 @@ const GroupLabel = forwardRef< HTMLDivElement, WordPressComponentProps< CompositeGroupLabelProps, 'div', false > >( function CompositeGroupLabel( props, ref ) { - return ; + const context = useCompositeContext(); + return ( + + ); } ); GroupLabel.displayName = 'Composite.GroupLabel'; @@ -88,7 +103,14 @@ const Item = forwardRef< HTMLButtonElement, WordPressComponentProps< CompositeItemProps, 'button', false > >( function CompositeItem( props, ref ) { - return ; + const context = useCompositeContext(); + return ( + + ); } ); Item.displayName = 'Composite.Item'; @@ -96,7 +118,14 @@ const Row = forwardRef< HTMLDivElement, WordPressComponentProps< CompositeRowProps, 'div', false > >( function CompositeRow( props, ref ) { - return ; + const context = useCompositeContext(); + return ( + + ); } ); Row.displayName = 'Composite.Row'; @@ -104,7 +133,14 @@ const Hover = forwardRef< HTMLDivElement, WordPressComponentProps< CompositeHoverProps, 'div', false > >( function CompositeHover( props, ref ) { - return ; + const context = useCompositeContext(); + return ( + + ); } ); Hover.displayName = 'Composite.Hover'; @@ -112,7 +148,14 @@ const Typeahead = forwardRef< HTMLDivElement, WordPressComponentProps< CompositeTypeaheadProps, 'div', false > >( function CompositeTypeahead( props, ref ) { - return ; + const context = useCompositeContext(); + return ( + + ); } ); Typeahead.displayName = 'Composite.Typeahead'; @@ -136,9 +179,28 @@ export const Composite = Object.assign( forwardRef< HTMLDivElement, WordPressComponentProps< CompositeProps, 'div', false > - >( function Composite( { disabled = false, ...props }, ref ) { + >( function Composite( + { children, store, disabled = false, ...props }, + ref + ) { + const contextValue = useMemo( + () => ( { + store, + } ), + [ store ] + ); + return ( - + + + { children } + + ); } ), { @@ -260,5 +322,20 @@ export const Composite = Object.assign( * ``` */ Typeahead, + /** + * The React context used by the composite components. It can be used by + * to access the composite store, and to forward the context when composite + * sub-components are rendered across portals (ie. `SlotFill` components) + * that would not otherwise forward the context to the `Fill` children. + * + * @example + * ```jsx + * import { Composite } from '@wordpress/components'; + * import { useContext } from '@wordpress/element'; + * + * const compositeContext = useContext( Composite.Context ); + * ``` + */ + Context: CompositeContext, } ); diff --git a/packages/components/src/composite/types.ts b/packages/components/src/composite/types.ts index 5afe410f7582ba..05a2b8473eb349 100644 --- a/packages/components/src/composite/types.ts +++ b/packages/components/src/composite/types.ts @@ -3,6 +3,15 @@ */ import type * as Ariakit from '@ariakit/react'; +export type CompositeContextProps = + | { + /** + * Object returned by the `useCompositeStore` hook. + */ + store: Ariakit.CompositeStore; + } + | undefined; + export type CompositeStoreProps = { /** * The current active item `id`. The active item is the element within the From 00d1f8afac2e3b9764dc28261462b72794ae5ce0 Mon Sep 17 00:00:00 2001 From: Luis Herranz Date: Wed, 14 Aug 2024 17:04:04 +0200 Subject: [PATCH 082/126] Add missing changes to the changelog for the PR #62734 (#64507) Co-authored-by: luisherranz Co-authored-by: SantosGuillamot --- packages/interactivity/CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/interactivity/CHANGELOG.md b/packages/interactivity/CHANGELOG.md index 6eb664984e1e53..52707b70ba5a7b 100644 --- a/packages/interactivity/CHANGELOG.md +++ b/packages/interactivity/CHANGELOG.md @@ -4,14 +4,24 @@ ## 6.5.0 (2024-08-07) +### Enhancements + +- Refactor internal proxy and signals system ([#62734](https://github.com/WordPress/gutenberg/pull/62734)). + +### Bug Fixes + +- Prevent overriding of existing properties on state and context after a client-side navigation ([#62734](https://github.com/WordPress/gutenberg/pull/62734)). + ## 6.4.0 (2024-07-24) ## 6.3.0 (2024-07-10) ## 6.2.0 (2024-06-26) + ### Enhancements - Export `splitTask` function from `@wordpress/interactivity` package to facilitate yielding to the main thread. See example in [async actions](https://github.com/WordPress/gutenberg/blob/trunk/docs/reference-guides/interactivity-api/api-reference.md#async-actions) documentation. ([#62665](https://github.com/WordPress/gutenberg/pull/62665)) + ## 6.1.0 (2024-06-15) ## 6.0.0 (2024-05-31) From 93b473840667751c655a9afbed2ebd2c1ade522e Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Wed, 14 Aug 2024 17:29:41 +0200 Subject: [PATCH 083/126] `Composite` v2: undo stabilizing new version (#64510) * Remove exports from `@wordpress/components` package * Update Storybook * Update CHANGELOG * README --- Co-authored-by: ciampo Co-authored-by: tyxla --- packages/components/CHANGELOG.md | 13 ++++++------- packages/components/src/composite/README.md | 4 ++++ .../src/composite/legacy/stories/index.story.tsx | 3 +-- .../src/composite/stories/index.story.tsx | 3 ++- packages/components/src/index.ts | 1 - 5 files changed, 13 insertions(+), 11 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 594bb47e187d51..7b3a2aea460d9f 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -19,13 +19,6 @@ - `ToggleGroupControl` - `TreeSelect` -### New Features - -- `Composite`: add stable version of the component ([#63564](https://github.com/WordPress/gutenberg/pull/63564)). -- `Composite`: add `Hover` and `Typeahead` subcomponents ([#64399](https://github.com/WordPress/gutenberg/pull/64399)). -- `Composite`: export `useCompositeStore`, add focus-related props to `Composite`and`Composite.Item` subcomponents ([#64450](https://github.com/WordPress/gutenberg/pull/64450)). -- `Composite`: add `Context` subcomponent ([#64493](https://github.com/WordPress/gutenberg/pull/64493)). - ### Enhancements - `Composite`: improve Storybook examples and add interactive controls ([#64397](https://github.com/WordPress/gutenberg/pull/64397)). @@ -34,6 +27,12 @@ - `TimePicker`: add `hideLabelFromVision` prop ([#64267](https://github.com/WordPress/gutenberg/pull/64267)). - `DropdownMenuV2`: adopt elevation scale ([#64432](https://github.com/WordPress/gutenberg/pull/64432)). +### Internal + +- `Composite` v2: add `Hover` and `Typeahead` subcomponents ([#64399](https://github.com/WordPress/gutenberg/pull/64399)). +- `Composite` v2: add focus-related props to `Composite`and`Composite.Item` subcomponents ([#64450](https://github.com/WordPress/gutenberg/pull/64450)). +- `Composite` v2: add `Context` subcomponent ([#64493](https://github.com/WordPress/gutenberg/pull/64493)). + ## 28.5.0 (2024-08-07) ### Bug Fixes diff --git a/packages/components/src/composite/README.md b/packages/components/src/composite/README.md index 76e345b16d13fa..35881d815cf1bc 100644 --- a/packages/components/src/composite/README.md +++ b/packages/components/src/composite/README.md @@ -1,5 +1,9 @@ # `Composite` +
+This feature is still experimental. “Experimental” means this is an early implementation subject to drastic and breaking changes. +
+ `Composite` provides a single tab stop on the page and allows navigation through the focusable descendants with arrow keys. This abstract component is based on the [WAI-ARIA Composite Role⁠](https://w3c.github.io/aria/#composite). ## Usage diff --git a/packages/components/src/composite/legacy/stories/index.story.tsx b/packages/components/src/composite/legacy/stories/index.story.tsx index 1b8e07e9bbf560..e46d656a16810e 100644 --- a/packages/components/src/composite/legacy/stories/index.story.tsx +++ b/packages/components/src/composite/legacy/stories/index.story.tsx @@ -15,8 +15,7 @@ import { import { UseCompositeStatePlaceholder, transform } from './utils'; const meta: Meta< typeof UseCompositeStatePlaceholder > = { - title: 'Components (Deprecated)/Composite (Unstable)', - id: 'components-composite-unstable', + title: 'Components/Composite', component: UseCompositeStatePlaceholder, subcomponents: { Composite, diff --git a/packages/components/src/composite/stories/index.story.tsx b/packages/components/src/composite/stories/index.story.tsx index 405962b92a761c..034e1d6721f7bd 100644 --- a/packages/components/src/composite/stories/index.story.tsx +++ b/packages/components/src/composite/stories/index.story.tsx @@ -15,7 +15,7 @@ import { Composite, useCompositeStore } from '..'; import { UseCompositeStorePlaceholder, transform } from './utils'; const meta: Meta< typeof UseCompositeStorePlaceholder > = { - title: 'Components/Composite', + title: 'Components/Composite (V2)', component: UseCompositeStorePlaceholder, subcomponents: { // @ts-expect-error - See https://github.com/storybookjs/storybook/issues/23170 @@ -50,6 +50,7 @@ const meta: Meta< typeof UseCompositeStorePlaceholder > = { options: [ 'horizontal', 'vertical', 'both' ], }, }, + tags: [ 'status-private' ], parameters: { controls: { expanded: true }, docs: { diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index cd6d2a77db9cb6..6483e34dc222a8 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -62,7 +62,6 @@ export { CompositeItem as __unstableCompositeItem, useCompositeState as __unstableUseCompositeState, } from './composite/legacy'; -export { Composite, useCompositeStore } from './composite'; export { ConfirmDialog as __experimentalConfirmDialog } from './confirm-dialog'; export { default as CustomSelectControl } from './custom-select-control'; export { default as Dashicon } from './dashicon'; From b8d195e3196c82318270f561b958b3da943a03e9 Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Thu, 15 Aug 2024 02:48:46 +0900 Subject: [PATCH 084/126] FocalPointPicker: Default to new 40px size (#64456) * FocalPointPicker: Default to new 40px size * Simplify prop passing * Remove unnecessary usage * Add changelog --------- Co-authored-by: mirka <0mirka00@git.wordpress.org> Co-authored-by: tyxla --- .../src/components/global-styles/background-panel.js | 1 - packages/block-library/src/cover/edit/inspector-controls.js | 1 - packages/block-library/src/media-text/edit.js | 1 - packages/components/CHANGELOG.md | 1 + packages/components/src/focal-point-picker/controls.tsx | 4 +--- packages/components/src/focal-point-picker/index.tsx | 2 -- packages/components/src/focal-point-picker/types.ts | 4 ++-- 7 files changed, 4 insertions(+), 10 deletions(-) diff --git a/packages/block-editor/src/components/global-styles/background-panel.js b/packages/block-editor/src/components/global-styles/background-panel.js index 906202206d1b76..14b37bb3007658 100644 --- a/packages/block-editor/src/components/global-styles/background-panel.js +++ b/packages/block-editor/src/components/global-styles/background-panel.js @@ -585,7 +585,6 @@ function BackgroundSizeControls( { return ( {}; export default function FocalPointPickerControls( { __nextHasNoMarginBottom, - __next40pxDefaultSize, hasHelpText, onChange = noop, point = { @@ -57,7 +56,6 @@ export default function FocalPointPickerControls( { gap={ 4 } > { diff --git a/packages/components/src/focal-point-picker/types.ts b/packages/components/src/focal-point-picker/types.ts index bd66ae02451a95..357af1e090d7f5 100644 --- a/packages/components/src/focal-point-picker/types.ts +++ b/packages/components/src/focal-point-picker/types.ts @@ -29,7 +29,8 @@ export type FocalPointPickerProps = Pick< /** * Start opting into the larger default height that will become the default size in a future version. * - * @default false + * @deprecated Default behavior since WP 6.7. Prop can be safely removed. + * @ignore */ __next40pxDefaultSize?: boolean; /** @@ -68,7 +69,6 @@ export type FocalPointPickerProps = Pick< export type FocalPointPickerControlsProps = { __nextHasNoMarginBottom?: boolean; - __next40pxDefaultSize?: boolean; /** * A bit of extra bottom margin will be added if a `help` text * needs to be rendered under it. From 1b6c106d704ad2eab5f576d13064f4ee1399ec61 Mon Sep 17 00:00:00 2001 From: Shail Mehta Date: Thu, 15 Aug 2024 02:14:20 +0530 Subject: [PATCH 085/126] Updated small typo in modularity.md (#64518) Co-authored-by: shail-mehta Co-authored-by: ndiego --- docs/explanations/architecture/modularity.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/explanations/architecture/modularity.md b/docs/explanations/architecture/modularity.md index f94f8ec7b9472e..ff619ccbfdf5b7 100644 --- a/docs/explanations/architecture/modularity.md +++ b/docs/explanations/architecture/modularity.md @@ -42,7 +42,7 @@ function MyApp() { ```php // myplugin.php -// Example of script registration dependending on the "components" and "element packages. +// Example of script registration depending on the "components" and "element packages. wp_register_script( 'myscript', 'pathtomyscript.js', array ('wp-components', "react" ) ); ``` From cf8aeba074334550bc42471b022dac7137aa1eb2 Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Wed, 14 Aug 2024 15:05:11 -0700 Subject: [PATCH 086/126] Make wp-env compatible with WordPress versions older than 5.4 by fixing wp-config anchors (#55864) * Make wp-env compatible with WordPress versions older than 5.4 by fixing wp-config anchors. * Use simple version compare function instead of semver library. * Fix lint violations. * Convert wpVersion to a string if needed. * Simplify utility function for WP version check. * Temp test. * Temp test. * Temp test. * Remove temp code. * Add missing spaces in WP 5.1-5.3 code. --- packages/env/lib/wordpress.js | 54 +++++++++++++++++++++++++++++++++-- 1 file changed, 52 insertions(+), 2 deletions(-) diff --git a/packages/env/lib/wordpress.js b/packages/env/lib/wordpress.js index 423547fad688b5..bd3c4a23f8ff5d 100644 --- a/packages/env/lib/wordpress.js +++ b/packages/env/lib/wordpress.js @@ -27,6 +27,30 @@ const { getCache, setCache } = require( './cache' ); * @typedef {'development'|'tests'|'all'} WPEnvironmentSelection */ +/** + * Utility function to check if a WordPress version is lower than another version. + * + * This is a non-comprehensive check only intended for this usage, to avoid pulling in a full semver library. + * It only considers the major and minor portions of the version and ignores the rest. Additionally, it assumes that + * the minor version is always a single digit (i.e. 0-9). + * + * Do not use this function for general version comparison, as it will not work for all cases. + * + * @param {string} version The version to check. + * @param {string} compareVersion The compare version to check whether the version is lower than. + * @return {boolean} True if the version is lower than the compare version, false otherwise. + */ +function isWPMajorMinorVersionLower( version, compareVersion ) { + const versionNumber = Number.parseFloat( + version.match( /^[0-9]+(\.[0-9]+)?/ )[ 0 ] + ); + const compareVersionNumber = Number.parseFloat( + compareVersion.match( /^[0-9]+(\.[0-9]+)?/ )[ 0 ] + ); + + return versionNumber < compareVersionNumber; +} + /** * Checks a WordPress database connection. An error is thrown if the test is * unsuccessful. @@ -51,11 +75,28 @@ async function checkDatabaseConnection( { dockerComposeConfigPath, debug } ) { * @param {Object} spinner A CLI spinner which indicates progress. */ async function configureWordPress( environment, config, spinner ) { + let wpVersion = ''; + try { + wpVersion = await readWordPressVersion( + config.env[ environment ].coreSource, + spinner, + config.debug + ); + } catch ( err ) { + // Ignore error. + } + const installCommand = `wp core install --url="${ config.env[ environment ].config.WP_SITEURL }" --title="${ config.name }" --admin_user=admin --admin_password=password --admin_email=wordpress@example.com --skip-email`; // -eo pipefail exits the command as soon as anything fails in bash. const setupCommands = [ 'set -eo pipefail', installCommand ]; + // WordPress versions below 5.1 didn't use proper spacing in wp-config. + const configAnchor = + wpVersion && isWPMajorMinorVersionLower( wpVersion, '5.1' ) + ? `"define('WP_DEBUG',"` + : `"define( 'WP_DEBUG',"`; + // Set wp-config.php values. for ( let [ key, value ] of Object.entries( config.env[ environment ].config @@ -68,7 +109,7 @@ async function configureWordPress( environment, config, spinner ) { // Add quotes around string values to work with multi-word strings better. value = typeof value === 'string' ? `"${ value }"` : value; setupCommands.push( - `wp config set ${ key } ${ value } --anchor="define( 'WP_DEBUG',"${ + `wp config set ${ key } ${ value } --anchor=${ configAnchor }${ typeof value !== 'string' ? ' --raw' : '' }` ); @@ -98,6 +139,15 @@ async function configureWordPress( environment, config, spinner ) { } ); + // WordPress versions below 5.1 didn't use proper spacing in wp-config. + // Additionally, WordPress versions below 5.4 used `dirname( __FILE__ )` instead of `__DIR__`. + let abspathDef = `define( 'ABSPATH', __DIR__ . '\\/' );`; + if ( wpVersion && isWPMajorMinorVersionLower( wpVersion, '5.1' ) ) { + abspathDef = `define('ABSPATH', dirname(__FILE__) . '\\/');`; + } else if ( wpVersion && isWPMajorMinorVersionLower( wpVersion, '5.4' ) ) { + abspathDef = `define( 'ABSPATH', dirname( __FILE__ ) . '\\/' );`; + } + // WordPress' PHPUnit suite expects a `wp-tests-config.php` in // the directory that the test suite is contained within. // Make sure ABSPATH points to the WordPress install. @@ -106,7 +156,7 @@ async function configureWordPress( environment, config, spinner ) { [ 'sh', '-c', - `sed -e "/^require.*wp-settings.php/d" -e "s/define( 'ABSPATH', __DIR__ . '\\/' );/define( 'ABSPATH', '\\/var\\/www\\/html\\/' );\\n\\tdefine( 'WP_DEFAULT_THEME', 'default' );/" /var/www/html/wp-config.php > /wordpress-phpunit/wp-tests-config.php`, + `sed -e "/^require.*wp-settings.php/d" -e "s/${ abspathDef }/define( 'ABSPATH', '\\/var\\/www\\/html\\/' );\\n\\tdefine( 'WP_DEFAULT_THEME', 'default' );/" /var/www/html/wp-config.php > /wordpress-phpunit/wp-tests-config.php`, ], { config: config.dockerComposeConfigPath, From 454129b07628e7fead84f158a4d05580ee5a6d3f Mon Sep 17 00:00:00 2001 From: Ramon Date: Thu, 15 Aug 2024 10:20:51 +1000 Subject: [PATCH 087/126] `RangeControl`: tweak mark and label absolute positioning (#64487) * This commit tweaks the position of the range control marks and labels to fit the original design. * CHANGELOG.md update Co-authored-by: ramonjd Co-authored-by: mirka <0mirka00@git.wordpress.org> Co-authored-by: jameskoster --- packages/components/CHANGELOG.md | 4 ++++ .../src/range-control/styles/range-control-styles.ts | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 2530b66152bdc2..f7b7bdc088cee9 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -28,6 +28,10 @@ - `FocalPointPicker`: Default to new 40px size ([#64456](https://github.com/WordPress/gutenberg/pull/64456)). - `DropdownMenuV2`: adopt elevation scale ([#64432](https://github.com/WordPress/gutenberg/pull/64432)). +### Bug Fixes + +- `RangeControl`: tweak mark and label absolute positioning ([#64487](https://github.com/WordPress/gutenberg/pull/64487)). + ### Internal - `Composite` v2: add `Hover` and `Typeahead` subcomponents ([#64399](https://github.com/WordPress/gutenberg/pull/64399)). diff --git a/packages/components/src/range-control/styles/range-control-styles.ts b/packages/components/src/range-control/styles/range-control-styles.ts index 89f4864aee2ea6..ec1572d2679247 100644 --- a/packages/components/src/range-control/styles/range-control-styles.ts +++ b/packages/components/src/range-control/styles/range-control-styles.ts @@ -154,7 +154,7 @@ export const Mark = styled.span` height: ${ thumbSize }px; left: 0; position: absolute; - top: -4px; + top: 9px; width: 1px; ${ markFill }; @@ -170,7 +170,7 @@ export const MarkLabel = styled.span` color: ${ COLORS.gray[ 300 ] }; font-size: 11px; position: absolute; - top: 12px; + top: 22px; white-space: nowrap; ${ rtl( { left: 0 } ) }; From add79ad40619242e31a1cc57f38d1aa44ebb9929 Mon Sep 17 00:00:00 2001 From: Ramon Date: Thu, 15 Aug 2024 12:42:09 +1000 Subject: [PATCH 088/126] Style engine: export util to compile CSS custom var from preset string (#64490) Remove `compileStyleValue` in favour of pre-existing `getCSSValueFromRawStyle()` from the style engine package. Update style engine tests to cover bugfix in https://github.com/WordPress/gutenberg/pull/43116 Moves style engine utils tests into correct directory Co-authored-by: ramonjd Co-authored-by: aaronrobertshaw Co-authored-by: kevin940726 --- .../global-styles/use-global-styles-output.js | 7 +-- .../src/components/global-styles/utils.js | 31 +--------- packages/style-engine/CHANGELOG.md | 4 ++ packages/style-engine/README.md | 16 +++++ packages/style-engine/src/index.ts | 3 + .../style-engine/src/styles/test/utils.js | 54 ++++++++++++++++ packages/style-engine/src/styles/utils.ts | 18 ++++-- packages/style-engine/src/test/utils.js | 62 ------------------- packages/style-engine/src/types.ts | 2 +- 9 files changed, 96 insertions(+), 101 deletions(-) create mode 100644 packages/style-engine/src/styles/test/utils.js delete mode 100644 packages/style-engine/src/test/utils.js diff --git a/packages/block-editor/src/components/global-styles/use-global-styles-output.js b/packages/block-editor/src/components/global-styles/use-global-styles-output.js index c1449d6d8b298f..cd4ad0cea50e0d 100644 --- a/packages/block-editor/src/components/global-styles/use-global-styles-output.js +++ b/packages/block-editor/src/components/global-styles/use-global-styles-output.js @@ -10,7 +10,7 @@ import { } from '@wordpress/blocks'; import { useSelect } from '@wordpress/data'; import { useContext, useMemo } from '@wordpress/element'; -import { getCSSRules } from '@wordpress/style-engine'; +import { getCSSRules, getCSSValueFromRawStyle } from '@wordpress/style-engine'; import { privateApis as componentsPrivateApis } from '@wordpress/components'; /** @@ -24,7 +24,6 @@ import { scopeFeatureSelectors, appendToSelector, getBlockStyleVariationSelector, - compileStyleValue, getResolvedValue, } from './utils'; import { getBlockCSSSelector } from './get-block-css-selector'; @@ -357,7 +356,7 @@ export function getStylesDeclarations( ? name : kebabCase( name ); declarations.push( - `${ cssProperty }: ${ compileStyleValue( + `${ cssProperty }: ${ getCSSValueFromRawStyle( getValueFromObjectPath( styleValue, [ prop ] ) ) }` ); @@ -369,7 +368,7 @@ export function getStylesDeclarations( ? key : kebabCase( key ); declarations.push( - `${ cssProperty }: ${ compileStyleValue( + `${ cssProperty }: ${ getCSSValueFromRawStyle( getValueFromObjectPath( blockStyles, pathToValue ) ) }` ); diff --git a/packages/block-editor/src/components/global-styles/utils.js b/packages/block-editor/src/components/global-styles/utils.js index 8de479e39382e5..4cd93357b081b3 100644 --- a/packages/block-editor/src/components/global-styles/utils.js +++ b/packages/block-editor/src/components/global-styles/utils.js @@ -7,6 +7,7 @@ import fastDeepEqual from 'fast-deep-equal/es6'; * WordPress dependencies */ import { useViewportMatch } from '@wordpress/compose'; +import { getCSSValueFromRawStyle } from '@wordpress/style-engine'; /** * Internal dependencies @@ -526,34 +527,6 @@ export function getBlockStyleVariationSelector( variation, blockSelector ) { return result.join( ',' ); } -/** - * Converts style preset values `var:` to CSS custom var values. - * TODO: Export and use the style engine util: getCSSVarFromStyleValue(). - * - * Example: - * - * compileStyleValue( 'var:preset|color|primary' ) // returns 'var(--wp--color-primary)' - * - * @param {string} uncompiledValue A block style value. - * @return {string} The compiled, or original value. - */ -export function compileStyleValue( uncompiledValue ) { - const VARIABLE_REFERENCE_PREFIX = 'var:'; - if ( - 'string' === typeof uncompiledValue && - uncompiledValue?.startsWith?.( VARIABLE_REFERENCE_PREFIX ) - ) { - const VARIABLE_PATH_SEPARATOR_TOKEN_ATTRIBUTE = '|'; - const VARIABLE_PATH_SEPARATOR_TOKEN_STYLE = '--'; - const variable = uncompiledValue - .slice( VARIABLE_REFERENCE_PREFIX.length ) - .split( VARIABLE_PATH_SEPARATOR_TOKEN_ATTRIBUTE ) - .join( VARIABLE_PATH_SEPARATOR_TOKEN_STYLE ); - return `var(--wp--${ variable })`; - } - return uncompiledValue; -} - /** * Looks up a theme file URI based on a relative path. * @@ -591,7 +564,7 @@ export function getResolvedRefValue( ruleValue, tree ) { if ( typeof ruleValue !== 'string' && ruleValue?.ref ) { const refPath = ruleValue.ref.split( '.' ); - const resolvedRuleValue = compileStyleValue( + const resolvedRuleValue = getCSSValueFromRawStyle( getValueFromObjectPath( tree, refPath ) ); diff --git a/packages/style-engine/CHANGELOG.md b/packages/style-engine/CHANGELOG.md index d393e427a63071..42928ac9fbd3c7 100644 --- a/packages/style-engine/CHANGELOG.md +++ b/packages/style-engine/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### New Features + +- Style engine: export util to compile CSS custom var from preset string. ([#64490](https://github.com/WordPress/gutenberg/pull/64490)) + ## 2.5.0 (2024-08-07) ## 2.4.0 (2024-07-24) diff --git a/packages/style-engine/README.md b/packages/style-engine/README.md index 44b78eed8a81a6..14e8edbd0dec26 100644 --- a/packages/style-engine/README.md +++ b/packages/style-engine/README.md @@ -258,6 +258,22 @@ _Changelog_ `6.1.0` Introduced in WordPress core. +### getCSSValueFromRawStyle + +Returns a WordPress CSS custom var value from incoming style preset value. The preset value follows the pattern `var:description|context|slug`. + +Example: + +`getCSSValueFromRawStyle( 'var:preset|color|heavenlyBlue' )` // returns 'var(--wp--preset--color--heavenly-blue)' + +_Parameters_ + +- _styleValue_ `string | any`: A raw style value. + +_Returns_ + +- `string | unknown`: A CSS var value. + ## Glossary diff --git a/packages/style-engine/src/index.ts b/packages/style-engine/src/index.ts index fe5b9929877be7..102d5842e7a0be 100644 --- a/packages/style-engine/src/index.ts +++ b/packages/style-engine/src/index.ts @@ -91,3 +91,6 @@ export function getCSSRules( return rules; } + +// Export style utils. +export { getCSSValueFromRawStyle } from './styles/utils'; diff --git a/packages/style-engine/src/styles/test/utils.js b/packages/style-engine/src/styles/test/utils.js new file mode 100644 index 00000000000000..02f0adf494e71f --- /dev/null +++ b/packages/style-engine/src/styles/test/utils.js @@ -0,0 +1,54 @@ +/** + * Internal dependencies + */ +import { camelCaseJoin, getCSSValueFromRawStyle, upperFirst } from '../utils'; + +describe( 'utils', () => { + describe( 'upperFirst()', () => { + it( 'should return an string with a capitalized first letter', () => { + expect( upperFirst( 'toontown' ) ).toEqual( 'Toontown' ); + } ); + } ); + + describe( 'camelCaseJoin()', () => { + it( 'should return a camelCase string', () => { + expect( camelCaseJoin( [ 'toon', 'town' ] ) ).toEqual( 'toonTown' ); + } ); + } ); + + describe( 'getCSSValueFromRawStyle()', () => { + it.each( [ + [ 'min(40%, 400px)', 'min(40%, 400px)' ], + [ + 'var(--wp--preset--color--yellow-bun)', + 'var:preset|color|yellow-bun', + ], + [ 'var(--wp--preset--font-size--h-1)', 'var:preset|font-size|h1' ], + [ + 'var(--wp--preset--font-size--1-px)', + 'var:preset|font-size|1px', + ], + [ + 'var(--wp--preset--color--orange-11-orange)', + 'var:preset|color|orange11orange', + ], + [ + 'var(--wp--preset--color--heavenly-blue)', + 'var:preset|color|heavenlyBlue', + ], + [ + 'var(--wp--preset--background--dark-secrets-100)', + 'var:preset|background|dark_Secrets_100', + ], + [ null, null ], + [ false, false ], + [ 1000, 1000 ], + [ undefined, undefined ], + ] )( + 'should return %s using an incoming value of %s', + ( expected, value ) => { + expect( getCSSValueFromRawStyle( value ) ).toEqual( expected ); + } + ); + } ); +} ); diff --git a/packages/style-engine/src/styles/utils.ts b/packages/style-engine/src/styles/utils.ts index 00e9dab8b5892b..0c9b40c384b101 100644 --- a/packages/style-engine/src/styles/utils.ts +++ b/packages/style-engine/src/styles/utils.ts @@ -61,7 +61,7 @@ export function generateRule( { selector: options?.selector, key: ruleKey, - value: getCSSVarFromStyleValue( styleValue ), + value: getCSSValueFromRawStyle( styleValue ), }, ] : []; @@ -103,7 +103,7 @@ export function generateBoxRules( } else { const sideRules = individualProperties.reduce( ( acc: GeneratedCSSRule[], side: string ) => { - const value: string | undefined = getCSSVarFromStyleValue( + const value = getCSSValueFromRawStyle( getStyleValueByPath( boxStyle, [ side ] ) ); if ( value ) { @@ -127,13 +127,21 @@ export function generateBoxRules( } /** - * Returns a CSS var value from incoming style value following the pattern `var:description|context|slug`. + * Returns a WordPress CSS custom var value from incoming style preset value. + * The preset value follows the pattern `var:description|context|slug`. + * + * Example: + * + * `getCSSValueFromRawStyle( 'var:preset|color|heavenlyBlue' )` // returns 'var(--wp--preset--color--heavenly-blue)' * * @param styleValue A raw style value. * - * @return string A CSS var value. + * @return A CSS var value. */ -export function getCSSVarFromStyleValue( styleValue: string ): string { + +export function getCSSValueFromRawStyle( + styleValue: string | any +): string | unknown { if ( typeof styleValue === 'string' && styleValue.startsWith( VARIABLE_REFERENCE_PREFIX ) diff --git a/packages/style-engine/src/test/utils.js b/packages/style-engine/src/test/utils.js deleted file mode 100644 index 9f1f84d2b45310..00000000000000 --- a/packages/style-engine/src/test/utils.js +++ /dev/null @@ -1,62 +0,0 @@ -/** - * Internal dependencies - */ -import { - camelCaseJoin, - getCSSVarFromStyleValue, - upperFirst, -} from '../styles/utils'; - -describe( 'utils', () => { - describe( 'upperFirst()', () => { - it( 'should return an string with a capitalized first letter', () => { - expect( upperFirst( 'toontown' ) ).toEqual( 'Toontown' ); - } ); - } ); - - describe( 'camelCaseJoin()', () => { - it( 'should return a camelCase string', () => { - expect( camelCaseJoin( [ 'toon', 'town' ] ) ).toEqual( 'toonTown' ); - } ); - } ); - - describe( 'getCSSVarFromStyleValue()', () => { - it( 'should return a compiled CSS var', () => { - expect( - getCSSVarFromStyleValue( 'var:preset|color|yellow-bun' ) - ).toEqual( 'var(--wp--preset--color--yellow-bun)' ); - } ); - - it( 'should kebab case numbers', () => { - expect( - getCSSVarFromStyleValue( 'var:preset|font-size|h1' ) - ).toEqual( 'var(--wp--preset--font-size--h-1)' ); - } ); - - it( 'should kebab case numbers as prefix', () => { - expect( - getCSSVarFromStyleValue( 'var:preset|font-size|1px' ) - ).toEqual( 'var(--wp--preset--font-size--1-px)' ); - } ); - - it( 'should kebab case both sides of numbers', () => { - expect( - getCSSVarFromStyleValue( 'var:preset|color|orange11orange' ) - ).toEqual( 'var(--wp--preset--color--orange-11-orange)' ); - } ); - - it( 'should kebab case camel case', () => { - expect( - getCSSVarFromStyleValue( 'var:preset|color|heavenlyBlue' ) - ).toEqual( 'var(--wp--preset--color--heavenly-blue)' ); - } ); - - it( 'should kebab case underscores', () => { - expect( - getCSSVarFromStyleValue( - 'var:preset|background|dark_Secrets_100' - ) - ).toEqual( 'var(--wp--preset--background--dark-secrets-100)' ); - } ); - } ); -} ); diff --git a/packages/style-engine/src/types.ts b/packages/style-engine/src/types.ts index 5b361836a8e375..8de623fcfcb2bf 100644 --- a/packages/style-engine/src/types.ts +++ b/packages/style-engine/src/types.ts @@ -94,7 +94,7 @@ export interface StyleOptions { export interface GeneratedCSSRule { selector?: string; - value: string; + value: string | unknown; /** * The CSS key in JS style attribute format, compatible with React. * E.g. `paddingTop` instead of `padding-top`. From de656233be92e37fd1a7fdb0e31cf87824f63776 Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Thu, 15 Aug 2024 10:18:30 +0700 Subject: [PATCH 089/126] List Item: Add border support (#63541) Co-authored-by: aaronrobertshaw Co-authored-by: andrewserong --- packages/block-library/src/list-item/block.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/block-library/src/list-item/block.json b/packages/block-library/src/list-item/block.json index fc7117d1b792fc..a4bf2351d97509 100644 --- a/packages/block-library/src/list-item/block.json +++ b/packages/block-library/src/list-item/block.json @@ -23,6 +23,12 @@ "anchor": true, "className": false, "splitting": true, + "__experimentalBorder": { + "color": true, + "radius": true, + "style": true, + "width": true + }, "color": { "gradients": true, "link": true, @@ -57,6 +63,7 @@ } }, "selectors": { - "root": ".wp-block-list > li" + "root": ".wp-block-list > li", + "border": ".wp-block-list:not(.wp-block-list .wp-block-list) > li" } } From e6057ce69fb2f743fb0c8a71494f56ff39f5a900 Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Thu, 15 Aug 2024 10:19:07 +0700 Subject: [PATCH 090/126] List: Add border support (#63540) Co-authored-by: aaronrobertshaw Co-authored-by: andrewserong Co-authored-by: carolinan --- packages/block-library/src/list/block.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/block-library/src/list/block.json b/packages/block-library/src/list/block.json index 8b100071c15ea9..ea07a0eb542df3 100644 --- a/packages/block-library/src/list/block.json +++ b/packages/block-library/src/list/block.json @@ -39,6 +39,12 @@ "supports": { "anchor": true, "html": false, + "__experimentalBorder": { + "color": true, + "radius": true, + "style": true, + "width": true + }, "typography": { "fontSize": true, "lineHeight": true, @@ -75,6 +81,9 @@ "clientNavigation": true } }, + "selectors": { + "border": ".wp-block-list:not(.wp-block-list .wp-block-list)" + }, "editorStyle": "wp-block-list-editor", "style": "wp-block-list" } From d669eb661c69af31816ead9359c5986cffcea301 Mon Sep 17 00:00:00 2001 From: Ramon Date: Thu, 15 Aug 2024 13:44:09 +1000 Subject: [PATCH 091/126] Background images: add support for theme.json ref value resolution (#64128) Resolves refs before passing styles to style engine in the WP_Theme_JSON class. Consolidates all resolutions in the background panel (image and ref). Removes useGlobalStyleLinks completely. Delete theme URI utils. Ensures that backgroundImage style objects are not merged, but replaced when merged theme.json configs. For example, an incoming config such as a saved user config should overwrite the background styles of the base theme.json, where it exists. --------- Co-authored-by: ramonjd Co-authored-by: aaronrobertshaw Co-authored-by: andrewserong Co-authored-by: kevin940726 --- backport-changelog/6.7/7137.md | 1 + lib/class-wp-theme-json-gutenberg.php | 68 +++++--- .../global-styles/background-panel.js | 74 +++++--- .../src/components/global-styles/hooks.js | 5 - .../src/components/global-styles/index.js | 1 - .../test/theme-file-uri-utils.js | 41 ----- .../test/use-global-styles-output.js | 16 ++ .../global-styles/theme-file-uri-utils.js | 18 -- .../src/components/global-styles/utils.js | 5 + packages/block-editor/src/hooks/background.js | 9 +- .../global-styles/background-panel.js | 3 - .../components/global-styles/screen-block.js | 3 - .../global-styles-provider/index.js | 19 ++- .../src/styles/background/index.ts | 26 ++- phpunit/class-wp-theme-json-test.php | 160 ++++++++++++++++++ 15 files changed, 313 insertions(+), 136 deletions(-) delete mode 100644 packages/block-editor/src/components/global-styles/test/theme-file-uri-utils.js delete mode 100644 packages/block-editor/src/components/global-styles/theme-file-uri-utils.js diff --git a/backport-changelog/6.7/7137.md b/backport-changelog/6.7/7137.md index 1eba52ebaf5087..00771b8bc6c21d 100644 --- a/backport-changelog/6.7/7137.md +++ b/backport-changelog/6.7/7137.md @@ -1,4 +1,5 @@ https://github.com/WordPress/wordpress-develop/pull/7137 +* https://github.com/WordPress/gutenberg/pull/64128 * https://github.com/WordPress/gutenberg/pull/64192 * https://github.com/WordPress/gutenberg/pull/64328 diff --git a/lib/class-wp-theme-json-gutenberg.php b/lib/class-wp-theme-json-gutenberg.php index 20ea31090407b4..756ef06c80aa87 100644 --- a/lib/class-wp-theme-json-gutenberg.php +++ b/lib/class-wp-theme-json-gutenberg.php @@ -2329,7 +2329,7 @@ protected static function flatten_tree( $tree, $prefix = '', $token = '--' ) { * ```php * array( * 'name' => 'property_name', - * 'value' => 'property_value, + * 'value' => 'property_value', * ) * ``` * @@ -2338,6 +2338,7 @@ protected static function flatten_tree( $tree, $prefix = '', $token = '--' ) { * @since 6.1.0 Added `$theme_json`, `$selector`, and `$use_root_padding` parameters. * @since 6.5.0 Output a `min-height: unset` rule when `aspect-ratio` is set. * @since 6.6.0 Passing current theme JSON settings to wp_get_typography_font_size_value(). Using style engine to correctly fetch background CSS values. + * @since 6.7.0 Allow ref resolution of background properties. * * @param array $styles Styles to process. * @param array $settings Theme settings. @@ -2381,21 +2382,28 @@ protected static function compute_style_properties( $styles, $settings = array() $root_variable_duplicates[] = substr( $css_property, $root_style_length ); } - // Processes background styles. - if ( 'background' === $value_path[0] && isset( $styles['background'] ) ) { - /* - * For user-uploaded images at the block level, assign defaults. - * Matches defaults applied in the editor and in block supports: background.php. - */ - if ( static::ROOT_BLOCK_SELECTOR !== $selector && ! empty( $styles['background']['backgroundImage']['id'] ) ) { - $styles['background']['backgroundSize'] = $styles['background']['backgroundSize'] ?? 'cover'; - // If the background size is set to `contain` and no position is set, set the position to `center`. - if ( 'contain' === $styles['background']['backgroundSize'] && empty( $styles['background']['backgroundPosition'] ) ) { - $styles['background']['backgroundPosition'] = '50% 50%'; - } + /* + * Processes background image styles. + * If the value is a URL, it will be converted to a CSS `url()` value. + * For an uploaded image (images with a database ID), apply size and position + * defaults equal to those applied in block supports in lib/background.php. + */ + if ( 'background-image' === $css_property && ! empty( $value ) ) { + $background_styles = gutenberg_style_engine_get_styles( + array( 'background' => array( 'backgroundImage' => $value ) ) + ); + + $value = $background_styles['declarations'][ $css_property ]; + } + if ( empty( $value ) && static::ROOT_BLOCK_SELECTOR !== $selector && ! empty( $styles['background']['backgroundImage']['id'] ) ) { + if ( 'background-size' === $css_property ) { + $value = 'cover'; + } + // If the background size is set to `contain` and no position is set, set the position to `center`. + if ( 'background-position' === $css_property ) { + $background_size = $styles['background']['backgroundSize'] ?? null; + $value = 'contain' === $background_size ? '50% 50%' : null; } - $background_styles = gutenberg_style_engine_get_styles( array( 'background' => $styles['background'] ) ); - $value = $background_styles['declarations'][ $css_property ] ?? $value; } // Skip if empty and not "0" or value represents array of longhand values. @@ -2463,6 +2471,7 @@ protected static function compute_style_properties( $styles, $settings = array() * @since 5.8.0 * @since 5.9.0 Added support for values of array type, which are returned as is. * @since 6.1.0 Added the `$theme_json` parameter. + * @since 6.7.0 Added support for background image refs * * @param array $styles Styles subtree. * @param array $path Which property to process. @@ -2479,15 +2488,17 @@ protected static function get_property_value( $styles, $path, $theme_json = null } /* - * This converts references to a path to the value at that path - * where the values is an array with a "ref" key, pointing to a path. + * Where the current value is an array with a 'ref' key pointing + * to a path, this converts that path into the value at that path. * For example: { "ref": "style.color.background" } => "#fff". */ if ( is_array( $value ) && isset( $value['ref'] ) ) { $value_path = explode( '.', $value['ref'] ); - $ref_value = _wp_array_get( $theme_json, $value_path ); + $ref_value = _wp_array_get( $theme_json, $value_path, null ); + // Background Image refs can refer to a string or an array containing a URL string. + $ref_value_url = $ref_value['url'] ?? null; // Only use the ref value if we find anything. - if ( ! empty( $ref_value ) && is_string( $ref_value ) ) { + if ( ! empty( $ref_value ) && ( is_string( $ref_value ) || is_string( $ref_value_url ) ) ) { $value = $ref_value; } @@ -3247,6 +3258,25 @@ public function merge( $incoming ) { } } } + + /* + * Style values are merged at the leaf level, however + * some values provide exceptions, namely style values that are + * objects and represent unique definitions for the style. + */ + $style_nodes = static::get_styles_block_nodes(); + foreach ( $style_nodes as $style_node ) { + $path = $style_node['path']; + /* + * Background image styles should be replaced, not merged, + * as they themselves are specific object definitions for the style. + */ + $background_image_path = array_merge( $path, static::PROPERTIES_METADATA['background-image'] ); + $content = _wp_array_get( $incoming_data, $background_image_path, null ); + if ( isset( $content ) ) { + _wp_array_set( $this->theme_json, $background_image_path, $content ); + } + } } /** diff --git a/packages/block-editor/src/components/global-styles/background-panel.js b/packages/block-editor/src/components/global-styles/background-panel.js index 14b37bb3007658..1373c54764d155 100644 --- a/packages/block-editor/src/components/global-styles/background-panel.js +++ b/packages/block-editor/src/components/global-styles/background-panel.js @@ -34,6 +34,7 @@ import { useRef, useState, useEffect, + useMemo, } from '@wordpress/element'; import { useDispatch, useSelect } from '@wordpress/data'; import { focus } from '@wordpress/dom'; @@ -42,11 +43,15 @@ import { isBlobURL } from '@wordpress/blob'; /** * Internal dependencies */ -import { useToolsPanelDropdownMenuProps } from './utils'; +import { useToolsPanelDropdownMenuProps, getResolvedValue } from './utils'; import { setImmutably } from '../../utils/object'; import MediaReplaceFlow from '../media-replace-flow'; import { store as blockEditorStore } from '../../store'; -import { getResolvedThemeFilePath } from './theme-file-uri-utils'; + +import { + globalStylesDataKey, + globalStylesLinksDataKey, +} from '../../store/private-keys'; const IMAGE_BACKGROUND_TYPE = 'image'; const DEFAULT_CONTROLS = { @@ -270,7 +275,6 @@ function BackgroundImageControls( { onRemoveImage = noop, onResetImage = noop, displayInPanel, - themeFileURIs, defaultValues, } ) { const mediaUpload = useSelect( @@ -404,10 +408,7 @@ function BackgroundImageControls( { name={ @@ -449,7 +450,6 @@ function BackgroundSizeControls( { style, inheritedValue, defaultValues, - themeFileURIs, } ) { const sizeValue = style?.background?.backgroundSize || @@ -587,7 +587,7 @@ function BackgroundSizeControls( { @@ -697,8 +697,44 @@ export default function BackgroundPanel( { defaultControls = DEFAULT_CONTROLS, defaultValues = {}, headerLabel = __( 'Background image' ), - themeFileURIs, } ) { + /* + * Resolve any inherited "ref" pointers. + * Should the block editor need resolved, inherited values + * across all controls, this could be abstracted into a hook, + * e.g., useResolveGlobalStyle + */ + const { globalStyles, _links } = useSelect( ( select ) => { + const { getSettings } = select( blockEditorStore ); + const _settings = getSettings(); + return { + globalStyles: _settings[ globalStylesDataKey ], + _links: _settings[ globalStylesLinksDataKey ], + }; + }, [] ); + const resolvedInheritedValue = useMemo( () => { + const resolvedValues = { + background: {}, + }; + + if ( ! inheritedValue?.background ) { + return inheritedValue; + } + + Object.entries( inheritedValue?.background ).forEach( + ( [ key, backgroundValue ] ) => { + resolvedValues.background[ key ] = getResolvedValue( + backgroundValue, + { + styles: globalStyles, + _links, + } + ); + } + ); + return resolvedValues; + }, [ globalStyles, _links, inheritedValue ] ); + const resetAllFilter = useCallback( ( previousValue ) => { return { ...previousValue, @@ -710,11 +746,11 @@ export default function BackgroundPanel( { onChange( setImmutably( value, [ 'background' ], {} ) ); const { title, url } = value?.background?.backgroundImage || { - ...inheritedValue?.background?.backgroundImage, + ...resolvedInheritedValue?.background?.backgroundImage, }; const hasImageValue = hasBackgroundImageValue( value ) || - hasBackgroundImageValue( inheritedValue ); + hasBackgroundImageValue( resolvedInheritedValue ); const imageValue = value?.background?.backgroundImage || @@ -756,10 +792,7 @@ export default function BackgroundPanel( { @@ -767,8 +800,7 @@ export default function BackgroundPanel( { { setIsDropDownOpen( false ); @@ -784,8 +816,7 @@ export default function BackgroundPanel( { panelId={ panelId } style={ value } defaultValues={ defaultValues } - inheritedValue={ inheritedValue } - themeFileURIs={ themeFileURIs } + inheritedValue={ resolvedInheritedValue } /> @@ -793,8 +824,7 @@ export default function BackgroundPanel( { { setIsDropDownOpen( false ); diff --git a/packages/block-editor/src/components/global-styles/hooks.js b/packages/block-editor/src/components/global-styles/hooks.js index a1a4fc1a0a6ae1..2be77aec18a2c6 100644 --- a/packages/block-editor/src/components/global-styles/hooks.js +++ b/packages/block-editor/src/components/global-styles/hooks.js @@ -209,11 +209,6 @@ export function useGlobalStyle( return [ result, setStyle ]; } -export function useGlobalStyleLinks() { - const { merged: mergedConfig } = useContext( GlobalStylesContext ); - return mergedConfig?._links; -} - /** * React hook that overrides a global settings object with block and element specific settings. * diff --git a/packages/block-editor/src/components/global-styles/index.js b/packages/block-editor/src/components/global-styles/index.js index 062df0a5606e90..8096a48569f199 100644 --- a/packages/block-editor/src/components/global-styles/index.js +++ b/packages/block-editor/src/components/global-styles/index.js @@ -3,7 +3,6 @@ export { useGlobalSetting, useGlobalStyle, useSettingsForBlockElement, - useGlobalStyleLinks, } from './hooks'; export { getBlockCSSSelector } from './get-block-css-selector'; export { diff --git a/packages/block-editor/src/components/global-styles/test/theme-file-uri-utils.js b/packages/block-editor/src/components/global-styles/test/theme-file-uri-utils.js deleted file mode 100644 index e239bb0941ea9e..00000000000000 --- a/packages/block-editor/src/components/global-styles/test/theme-file-uri-utils.js +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Internal dependencies - */ -import { getResolvedThemeFilePath } from '../theme-file-uri-utils'; - -const themeFileURIs = [ - { - name: 'file:./assets/image.jpg', - href: 'https://wordpress.org/assets/image.jpg', - target: 'styles.background.backgroundImage.url', - }, - { - name: 'file:./assets/other/image.jpg', - href: 'https://wordpress.org/assets/other/image.jpg', - target: "styles.blocks.['core/group].background.backgroundImage.url", - }, -]; - -describe( 'getResolvedThemeFilePath()', () => { - it.each( [ - [ - 'file:./assets/image.jpg', - 'https://wordpress.org/assets/image.jpg', - 'Should return absolute URL if found in themeFileURIs', - ], - [ - 'file:./misc/image.jpg', - 'file:./misc/image.jpg', - 'Should return value if not found in themeFileURIs', - ], - [ - 'https://wordpress.org/assets/image.jpg', - 'https://wordpress.org/assets/image.jpg', - 'Should not match absolute URLs', - ], - ] )( 'Given file %s and return value %s: %s', ( file, returnedValue ) => { - expect( - getResolvedThemeFilePath( file, themeFileURIs ) === returnedValue - ).toBe( true ); - } ); -} ); diff --git a/packages/block-editor/src/components/global-styles/test/use-global-styles-output.js b/packages/block-editor/src/components/global-styles/test/use-global-styles-output.js index 1b061f6921f2c2..5022e8ba591dbb 100644 --- a/packages/block-editor/src/components/global-styles/test/use-global-styles-output.js +++ b/packages/block-editor/src/components/global-styles/test/use-global-styles-output.js @@ -1008,9 +1008,23 @@ describe( 'global styles renderer', () => { ref: 'styles.elements.h1.typography.letterSpacing', }, }, + background: { + backgroundImage: { + ref: 'styles.background.backgroundImage', + }, + backgroundSize: { + ref: 'styles.background.backgroundSize', + }, + }, }; const tree = { styles: { + background: { + backgroundImage: { + url: 'http://my-image.org/image.gif', + }, + backgroundSize: 'cover', + }, elements: { h1: { typography: { @@ -1026,6 +1040,8 @@ describe( 'global styles renderer', () => { ).toEqual( [ 'font-size: var(--wp--preset--font-size--xx-large)', 'letter-spacing: 2px', + "background-image: url( 'http://my-image.org/image.gif' )", + 'background-size: cover', ] ); } ); it( 'should set default values for block background styles', () => { diff --git a/packages/block-editor/src/components/global-styles/theme-file-uri-utils.js b/packages/block-editor/src/components/global-styles/theme-file-uri-utils.js deleted file mode 100644 index 96b3e2e4cb68b0..00000000000000 --- a/packages/block-editor/src/components/global-styles/theme-file-uri-utils.js +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Looks up a theme file URI based on a relative path. - * - * @param {string} file A relative path. - * @param {Array} themeFileURIs A collection of absolute theme file URIs and their corresponding file paths. - * @return {string?} A resolved theme file URI, if one is found in the themeFileURIs collection. - */ -export function getResolvedThemeFilePath( file, themeFileURIs = [] ) { - const uri = themeFileURIs.find( - ( themeFileUri ) => themeFileUri.name === file - ); - - if ( ! uri?.href ) { - return file; - } - - return uri?.href; -} diff --git a/packages/block-editor/src/components/global-styles/utils.js b/packages/block-editor/src/components/global-styles/utils.js index 4cd93357b081b3..59799c9032c67f 100644 --- a/packages/block-editor/src/components/global-styles/utils.js +++ b/packages/block-editor/src/components/global-styles/utils.js @@ -562,6 +562,11 @@ export function getResolvedRefValue( ruleValue, tree ) { return ruleValue; } + /* + * Where the rule value is an object with a 'ref' property pointing + * to a path, this converts that path into the value at that path. + * For example: { "ref": "style.color.background" } => "#fff". + */ if ( typeof ruleValue !== 'string' && ruleValue?.ref ) { const refPath = ruleValue.ref.split( '.' ); const resolvedRuleValue = getCSSValueFromRawStyle( diff --git a/packages/block-editor/src/hooks/background.js b/packages/block-editor/src/hooks/background.js index 0d38068cdefeea..3755aecbcb9d0b 100644 --- a/packages/block-editor/src/hooks/background.js +++ b/packages/block-editor/src/hooks/background.js @@ -16,10 +16,7 @@ import { useHasBackgroundPanel, hasBackgroundImageValue, } from '../components/global-styles/background-panel'; -import { - globalStylesDataKey, - globalStylesLinksDataKey, -} from '../store/private-keys'; +import { globalStylesDataKey } from '../store/private-keys'; export const BACKGROUND_SUPPORT_KEY = 'background'; @@ -136,14 +133,13 @@ export function BackgroundImagePanel( { setAttributes, settings, } ) { - const { style, inheritedValue, _links } = useSelect( + const { style, inheritedValue } = useSelect( ( select ) => { const { getBlockAttributes, getSettings } = select( blockEditorStore ); const _settings = getSettings(); return { style: getBlockAttributes( clientId )?.style, - _links: _settings[ globalStylesLinksDataKey ], /* * To ensure we pass down the right inherited values: * @TODO 1. Pass inherited value down to all block style controls, @@ -190,7 +186,6 @@ export function BackgroundImagePanel( { settings={ updatedSettings } onChange={ onChange } value={ style } - themeFileURIs={ _links?.[ 'wp:theme-file' ] } /> ); } diff --git a/packages/edit-site/src/components/global-styles/background-panel.js b/packages/edit-site/src/components/global-styles/background-panel.js index 24ab914bed8c52..e185079d8cee04 100644 --- a/packages/edit-site/src/components/global-styles/background-panel.js +++ b/packages/edit-site/src/components/global-styles/background-panel.js @@ -16,7 +16,6 @@ const BACKGROUND_DEFAULT_VALUES = { const { useGlobalStyle, useGlobalSetting, - useGlobalStyleLinks, BackgroundPanel: StylesBackgroundPanel, } = unlock( blockEditorPrivateApis ); @@ -42,7 +41,6 @@ export default function BackgroundPanel() { const [ inheritedStyle, setStyle ] = useGlobalStyle( '', undefined, 'all', { shouldDecodeEncode: false, } ); - const _links = useGlobalStyleLinks(); const [ settings ] = useGlobalSetting( '' ); return ( @@ -52,7 +50,6 @@ export default function BackgroundPanel() { onChange={ setStyle } settings={ settings } defaultValues={ BACKGROUND_DEFAULT_VALUES } - themeFileURIs={ _links?.[ 'wp:theme-file' ] } /> ); } diff --git a/packages/edit-site/src/components/global-styles/screen-block.js b/packages/edit-site/src/components/global-styles/screen-block.js index dee921f37f1e8a..b1489167f2dc75 100644 --- a/packages/edit-site/src/components/global-styles/screen-block.js +++ b/packages/edit-site/src/components/global-styles/screen-block.js @@ -85,7 +85,6 @@ const { FiltersPanel: StylesFiltersPanel, ImageSettingsPanel, AdvancedPanel: StylesAdvancedPanel, - useGlobalStyleLinks, } = unlock( blockEditorPrivateApis ); function ScreenBlock( { name, variation } ) { @@ -105,7 +104,6 @@ function ScreenBlock( { name, variation } ) { const [ rawSettings, setSettings ] = useGlobalSetting( '', name ); const settings = useSettingsForBlockElement( rawSettings, name ); const blockType = getBlockType( name ); - const _links = useGlobalStyleLinks(); // Only allow `blockGap` support if serialization has not been skipped, to be sure global spacing can be applied. if ( @@ -272,7 +270,6 @@ function ScreenBlock( { name, variation } ) { onChange={ setStyle } settings={ settings } defaultValues={ BACKGROUND_BLOCK_DEFAULT_VALUES } - themeFileURIs={ _links?.[ 'wp:theme-file' ] } /> ) } { hasTypographyPanel && ( diff --git a/packages/editor/src/components/global-styles-provider/index.js b/packages/editor/src/components/global-styles-provider/index.js index 8ac292fb2ce0b2..8426593d8f5f51 100644 --- a/packages/editor/src/components/global-styles-provider/index.js +++ b/packages/editor/src/components/global-styles-provider/index.js @@ -23,10 +23,23 @@ const { GlobalStylesContext, cleanEmptyObject } = unlock( export function mergeBaseAndUserConfigs( base, user ) { return deepmerge( base, user, { - // We only pass as arrays the presets, - // in which case we want the new array of values - // to override the old array (no merging). + /* + * We only pass as arrays the presets, + * in which case we want the new array of values + * to override the old array (no merging). + */ isMergeableObject: isPlainObject, + /* + * Exceptions to the above rule. + * Background images should be replaced, not merged, + * as they themselves are specific object definitions for the style. + */ + customMerge: ( key ) => { + if ( key === 'backgroundImage' ) { + return ( baseConfig, userConfig ) => userConfig; + } + return undefined; + }, } ); } diff --git a/packages/style-engine/src/styles/background/index.ts b/packages/style-engine/src/styles/background/index.ts index 211b97343d89cc..b943032f9c4417 100644 --- a/packages/style-engine/src/styles/background/index.ts +++ b/packages/style-engine/src/styles/background/index.ts @@ -8,6 +8,12 @@ const backgroundImage = { name: 'backgroundImage', generate: ( style: Style, options: StyleOptions ) => { const _backgroundImage = style?.background?.backgroundImage; + + /* + * The background image can be a string or an object. + * If the background image is a string, it could already contain a url() function, + * or have a linear-gradient value. + */ if ( typeof _backgroundImage === 'object' && _backgroundImage?.url ) { return [ { @@ -21,20 +27,12 @@ const backgroundImage = { ]; } - /* - * If the background image is a string, it could already contain a url() function, - * or have a linear-gradient value. - */ - if ( typeof _backgroundImage === 'string' ) { - return generateRule( - style, - options, - [ 'background', 'backgroundImage' ], - 'backgroundImage' - ); - } - - return []; + return generateRule( + style, + options, + [ 'background', 'backgroundImage' ], + 'backgroundImage' + ); }, }; diff --git a/phpunit/class-wp-theme-json-test.php b/phpunit/class-wp-theme-json-test.php index 3f11dd97a6688f..10bb47b87fba89 100644 --- a/phpunit/class-wp-theme-json-test.php +++ b/phpunit/class-wp-theme-json-test.php @@ -2294,6 +2294,112 @@ public function test_merge_incoming_data_presets_use_default_names() { $this->assertSameSetsWithIndex( $expected, $actual ); } + public function test_merge_incoming_background_styles() { + $theme_json = new WP_Theme_JSON_Gutenberg( + array( + 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, + 'styles' => array( + 'background' => array( + 'backgroundImage' => array( + 'url' => 'http://example.org/quote.png', + ), + 'backgroundSize' => 'cover', + ), + 'blocks' => array( + 'core/group' => array( + 'background' => array( + 'backgroundImage' => array( + 'ref' => 'styles.blocks.core/verse.background.backgroundImage', + ), + 'backgroundAttachment' => 'fixed', + ), + ), + 'core/quote' => array( + 'background' => array( + 'backgroundImage' => array( + 'url' => 'http://example.org/quote.png', + ), + 'backgroundAttachment' => array( + 'ref' => 'styles.blocks.core/group.background.backgroundAttachment', + ), + ), + ), + ), + ), + ) + ); + + $update_background_image_styles = array( + 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, + 'styles' => array( + 'background' => array( + 'backgroundSize' => 'contain', + ), + 'blocks' => array( + 'core/group' => array( + 'background' => array( + 'backgroundImage' => array( + 'url' => 'http://example.org/group.png', + ), + ), + ), + 'core/quote' => array( + 'background' => array( + 'backgroundAttachment' => 'fixed', + ), + ), + 'core/verse' => array( + 'background' => array( + 'backgroundImage' => array( + 'ref' => 'styles.blocks.core/group.background.backgroundImage', + ), + ), + ), + ), + ), + ); + $expected = array( + 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, + 'styles' => array( + 'background' => array( + 'backgroundImage' => array( + 'url' => 'http://example.org/quote.png', + ), + 'backgroundSize' => 'contain', + ), + 'blocks' => array( + 'core/group' => array( + 'background' => array( + 'backgroundImage' => array( + 'url' => 'http://example.org/group.png', + ), + 'backgroundAttachment' => 'fixed', + ), + ), + 'core/quote' => array( + 'background' => array( + 'backgroundImage' => array( + 'url' => 'http://example.org/quote.png', + ), + 'backgroundAttachment' => 'fixed', + ), + ), + 'core/verse' => array( + 'background' => array( + 'backgroundImage' => array( + 'ref' => 'styles.blocks.core/group.background.backgroundImage', + ), + ), + ), + ), + ), + ); + $theme_json->merge( new WP_Theme_JSON_Gutenberg( $update_background_image_styles ) ); + $actual = $theme_json->get_raw_data(); + + $this->assertEqualSetsWithIndex( $expected, $actual ); + } + public function test_remove_insecure_properties_removes_unsafe_styles() { $actual = WP_Theme_JSON_Gutenberg::remove_insecure_properties( array( @@ -4915,6 +5021,60 @@ public function test_get_block_background_image_styles() { $this->assertSameCSS( $verse_styles, $theme_json->get_styles_for_block( $verse_node ), 'Styles returned from "::get_styles_for_block()" with default core/verse background styles as string type do not match expectations' ); } + /** + * Testing background dynamic properties in theme.json. + */ + public function test_get_resolved_background_image_styles() { + $theme_json = new WP_Theme_JSON_Gutenberg( + array( + 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, + 'styles' => array( + 'background' => array( + 'backgroundImage' => array( + 'url' => 'http://example.org/top.png', + ), + 'backgroundSize' => 'contain', + 'backgroundRepeat' => 'repeat', + 'backgroundPosition' => '10% 20%', + 'backgroundAttachment' => 'scroll', + ), + 'blocks' => array( + 'core/group' => array( + 'background' => array( + 'backgroundImage' => array( + 'id' => 123, + 'url' => 'http://example.org/group.png', + ), + ), + ), + 'core/post-content' => array( + 'background' => array( + 'backgroundImage' => array( + 'ref' => 'styles.background.backgroundImage', + ), + 'backgroundSize' => array( + 'ref' => 'styles.background.backgroundSize', + ), + 'backgroundRepeat' => array( + 'ref' => 'styles.background.backgroundRepeat', + ), + 'backgroundPosition' => array( + 'ref' => 'styles.background.backgroundPosition', + ), + 'backgroundAttachment' => array( + 'ref' => 'styles.background.backgroundAttachment', + ), + ), + ), + ), + ), + ) + ); + + $expected = "html{min-height: calc(100% - var(--wp-admin--admin-bar--height, 0px));}body{background-image: url('http://example.org/top.png');background-position: 10% 20%;background-repeat: repeat;background-size: contain;background-attachment: scroll;}:root :where(.wp-block-group){background-image: url('http://example.org/group.png');background-size: cover;}:root :where(.wp-block-post-content){background-image: url('http://example.org/top.png');background-position: 10% 20%;background-repeat: repeat;background-size: contain;background-attachment: scroll;}"; + $this->assertSameCSS( $expected, $theme_json->get_stylesheet( array( 'styles' ), null, array( 'skip_root_layout_styles' => true ) ) ); + } + /** * Tests that base custom CSS is generated correctly. */ From c8eaf5c0c4f4d0bc07fb39626ae3268bd83fbcfe Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Thu, 15 Aug 2024 11:49:14 +0800 Subject: [PATCH 092/126] Components: Allow `style` prop on `Popover` (#64489) * Allow style prop on popover * Ensure styles are not overwritten by popoverProps * Add tests and fix overriding of built-in popover styles * Update comment * Remove blank line * Un-should tests * Update test to use `not.toHaveStyle * Add changelog entry * Remove `style` from types file ---- Co-authored-by: talldan Co-authored-by: mirka <0mirka00@git.wordpress.org> --- packages/components/CHANGELOG.md | 2 ++ packages/components/src/popover/index.tsx | 12 +++++-- .../components/src/popover/test/index.tsx | 34 +++++++++++++++++++ 3 files changed, 45 insertions(+), 3 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index f7b7bdc088cee9..e758e457bdd03e 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -27,6 +27,8 @@ - `TimePicker`: add `hideLabelFromVision` prop ([#64267](https://github.com/WordPress/gutenberg/pull/64267)). - `FocalPointPicker`: Default to new 40px size ([#64456](https://github.com/WordPress/gutenberg/pull/64456)). - `DropdownMenuV2`: adopt elevation scale ([#64432](https://github.com/WordPress/gutenberg/pull/64432)). +- `Popover`: allow `style` prop usage ([#64489](https://github.com/WordPress/gutenberg/pull/64489)). + ### Bug Fixes diff --git a/packages/components/src/popover/index.tsx b/packages/components/src/popover/index.tsx index 68d331ce8266ba..3005f13c952ec0 100644 --- a/packages/components/src/popover/index.tsx +++ b/packages/components/src/popover/index.tsx @@ -113,8 +113,9 @@ const UnforwardedPopover = ( WordPressComponentProps< PopoverProps, 'div', false >, // To avoid overlaps between the standard HTML attributes and the props // expected by `framer-motion`, omit all framer motion props from popover - // props (except for `animate` and `children`, which are re-defined in `PopoverProps`). - keyof Omit< MotionProps, 'animate' | 'children' > + // props (except for `animate` and `children` which are re-defined in + // `PopoverProps`, and `style` which is merged safely). + keyof Omit< MotionProps, 'animate' | 'children' | 'style' > >, forwardedRef: ForwardedRef< any > ) => { @@ -139,6 +140,7 @@ const UnforwardedPopover = ( shift = false, inline = false, variant, + style: contentStyle, // Deprecated props __unstableForcePosition, @@ -370,6 +372,7 @@ const UnforwardedPopover = ( const animationProps: HTMLMotionProps< 'div' > = shouldAnimate ? { style: { + ...contentStyle, ...motionInlineStyles, ...style, }, @@ -378,7 +381,10 @@ const UnforwardedPopover = ( } : { animate: false, - style, + style: { + ...contentStyle, + ...style, + }, }; // When Floating UI has finished positioning and Framer Motion has finished animating diff --git a/packages/components/src/popover/test/index.tsx b/packages/components/src/popover/test/index.tsx index f5ee7e96e4c54d..eac0f942df2f6e 100644 --- a/packages/components/src/popover/test/index.tsx +++ b/packages/components/src/popover/test/index.tsx @@ -179,6 +179,40 @@ describe( 'Popover', () => { } ); } ); + describe( 'style', () => { + it( 'outputs inline styles added through the style prop in addition to built-in popover positioning styles', async () => { + render( + + Hello + + ); + const popover = screen.getByTestId( 'popover-element' ); + + await waitFor( () => expect( popover ).toBeVisible() ); + expect( popover ).toHaveStyle( + 'position: absolute; top: 0px; left: 0px; z-index: 0;' + ); + } ); + + it( 'is not possible to override built-in popover positioning styles via the style prop', async () => { + render( + + Hello + + ); + const popover = screen.getByTestId( 'popover-element' ); + + await waitFor( () => expect( popover ).toBeVisible() ); + expect( popover ).not.toHaveStyle( 'position: static;' ); + } ); + } ); + describe( 'focus behavior', () => { it( 'should focus the popover container when opened', async () => { render( From 9a9186cd09424a91e3cf59288e2290d6d6867f73 Mon Sep 17 00:00:00 2001 From: Shail Mehta Date: Thu, 15 Aug 2024 10:25:18 +0530 Subject: [PATCH 093/126] Comment Content: Add Border Support (#64230) * Commtent Content: Add Border Support * fixed lint issue * Commtent Content: Add Border Support * Added style at root level * fixed lint issue * Added Border box in Editor Side * Added suggested changes * Fixed Conflict Issue * Removed Duplicate css * remove editor style Co-authored-by: shail-mehta Co-authored-by: t-hamano Co-authored-by: aaronrobertshaw --- .../block-library/src/comment-content/block.json | 15 ++++++++++++++- .../block-library/src/comment-content/style.scss | 5 +++++ packages/block-library/src/style.scss | 1 + 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/packages/block-library/src/comment-content/block.json b/packages/block-library/src/comment-content/block.json index 9ac4b5453365bb..0e812299a45e89 100644 --- a/packages/block-library/src/comment-content/block.json +++ b/packages/block-library/src/comment-content/block.json @@ -35,6 +35,18 @@ "fontSize": true } }, + "__experimentalBorder": { + "radius": true, + "color": true, + "width": true, + "style": true, + "__experimentalDefaultControls": { + "radius": true, + "color": true, + "width": true, + "style": true + } + }, "spacing": { "padding": [ "horizontal", "vertical" ], "__experimentalDefaultControls": { @@ -42,5 +54,6 @@ } }, "html": false - } + }, + "style": "wp-block-comment-content" } diff --git a/packages/block-library/src/comment-content/style.scss b/packages/block-library/src/comment-content/style.scss index dd9c4b22ae99c1..a94048de6014d3 100644 --- a/packages/block-library/src/comment-content/style.scss +++ b/packages/block-library/src/comment-content/style.scss @@ -3,3 +3,8 @@ font-size: 0.875em; line-height: 1.5; } + +.wp-block-comment-content { + // This block has customizable padding, border-box makes that more predictable. + box-sizing: border-box; +} diff --git a/packages/block-library/src/style.scss b/packages/block-library/src/style.scss index df1337d7eb25c0..86fe5a3eefabaf 100644 --- a/packages/block-library/src/style.scss +++ b/packages/block-library/src/style.scss @@ -11,6 +11,7 @@ @import "./comments-pagination/style.scss"; @import "./comment-template/style.scss"; @import "./comment-date/style.scss"; +@import "./comment-content/style.scss"; @import "./cover/style.scss"; @import "./details/style.scss"; @import "./embed/style.scss"; From 38c233fef036073ce4aded4ae9f5ea3b43a45d48 Mon Sep 17 00:00:00 2001 From: Ramon Date: Thu, 15 Aug 2024 16:12:15 +1000 Subject: [PATCH 094/126] Style engine: update type for getCSSValueFromRawStyle (#64528) * Use a generic type parameter StyleValue to describe argument and return value. By default, StyleValue is set to string. This allows the function to be flexible with the type of styleValue, but it defaults to handling strings. Co-authored-by: ramonjd Co-authored-by: kevin940726 --- packages/style-engine/README.md | 8 +++++--- packages/style-engine/src/styles/utils.ts | 18 ++++++++++-------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/packages/style-engine/README.md b/packages/style-engine/README.md index 14e8edbd0dec26..a3e302fab4bb9a 100644 --- a/packages/style-engine/README.md +++ b/packages/style-engine/README.md @@ -260,7 +260,9 @@ _Changelog_ ### getCSSValueFromRawStyle -Returns a WordPress CSS custom var value from incoming style preset value. The preset value follows the pattern `var:description|context|slug`. +Returns a WordPress CSS custom var value from incoming style preset value, if one is detected. + +The preset value is a string and follows the pattern `var:description|context|slug`. Example: @@ -268,11 +270,11 @@ Example: _Parameters_ -- _styleValue_ `string | any`: A raw style value. +- _styleValue_ `StyleValue`: A string representing a raw CSS value. Non-strings won't be processed. _Returns_ -- `string | unknown`: A CSS var value. +- `StyleValue`: A CSS custom var value if the incoming style value is a preset value. diff --git a/packages/style-engine/src/styles/utils.ts b/packages/style-engine/src/styles/utils.ts index 0c9b40c384b101..74a18d2c2a6e45 100644 --- a/packages/style-engine/src/styles/utils.ts +++ b/packages/style-engine/src/styles/utils.ts @@ -127,21 +127,23 @@ export function generateBoxRules( } /** - * Returns a WordPress CSS custom var value from incoming style preset value. - * The preset value follows the pattern `var:description|context|slug`. + * Returns a WordPress CSS custom var value from incoming style preset value, + * if one is detected. + * + * The preset value is a string and follows the pattern `var:description|context|slug`. * * Example: * * `getCSSValueFromRawStyle( 'var:preset|color|heavenlyBlue' )` // returns 'var(--wp--preset--color--heavenly-blue)' * - * @param styleValue A raw style value. + * @param styleValue A string representing a raw CSS value. Non-strings won't be processed. * - * @return A CSS var value. + * @return A CSS custom var value if the incoming style value is a preset value. */ -export function getCSSValueFromRawStyle( - styleValue: string | any -): string | unknown { +export function getCSSValueFromRawStyle< StyleValue = string >( + styleValue: StyleValue +): StyleValue { if ( typeof styleValue === 'string' && styleValue.startsWith( VARIABLE_REFERENCE_PREFIX ) @@ -160,7 +162,7 @@ export function getCSSValueFromRawStyle( } ) ) .join( VARIABLE_PATH_SEPARATOR_TOKEN_STYLE ); - return `var(--wp--${ variable })`; + return `var(--wp--${ variable })` as StyleValue; } return styleValue; } From 8fc4c29ef51b1e0dd07000ee4374116a7bad3b71 Mon Sep 17 00:00:00 2001 From: Robert Anderson Date: Thu, 15 Aug 2024 16:18:53 +1000 Subject: [PATCH 095/126] GridPopover: Avoid over-selecting by using a new getBlockStyles private selector (#64386) * Add getBlockStyles selector so avoid over-selecting in GridPopover * Will an empty commit make the tests run? --- .../src/components/grid/grid-visualizer.js | 17 ++-- .../src/store/private-selectors.js | 21 +++++ .../src/store/test/private-selectors.js | 89 +++++++++++++++++++ 3 files changed, 122 insertions(+), 5 deletions(-) diff --git a/packages/block-editor/src/components/grid/grid-visualizer.js b/packages/block-editor/src/components/grid/grid-visualizer.js index fad2f5cfb14835..81da0457ffc5ca 100644 --- a/packages/block-editor/src/components/grid/grid-visualizer.js +++ b/packages/block-editor/src/components/grid/grid-visualizer.js @@ -19,6 +19,7 @@ import { range, GridRect, getGridInfo } from './utils'; import { store as blockEditorStore } from '../../store'; import { useGetNumberOfBlocksBeforeCell } from './use-get-number-of-blocks-before-cell'; import ButtonBlockAppender from '../button-block-appender'; +import { unlock } from '../../lock-unlock'; export function GridVisualizer( { clientId, contentRef, parentLayout } ) { const isDistractionFree = useSelect( @@ -118,19 +119,25 @@ const GridVisualizerGrid = forwardRef( function ManualGridVisualizer( { gridClientId, gridInfo } ) { const [ highlightedRect, setHighlightedRect ] = useState( null ); - const gridItems = useSelect( - ( select ) => select( blockEditorStore ).getBlocks( gridClientId ), + const gridItemStyles = useSelect( + ( select ) => { + const { getBlockOrder, getBlockStyles } = unlock( + select( blockEditorStore ) + ); + const blockOrder = getBlockOrder( gridClientId ); + return getBlockStyles( blockOrder ); + }, [ gridClientId ] ); const occupiedRects = useMemo( () => { const rects = []; - for ( const block of gridItems ) { + for ( const style of Object.values( gridItemStyles ) ) { const { columnStart, rowStart, columnSpan = 1, rowSpan = 1, - } = block.attributes.style?.layout || {}; + } = style?.layout ?? {}; if ( ! columnStart || ! rowStart ) { continue; } @@ -144,7 +151,7 @@ function ManualGridVisualizer( { gridClientId, gridInfo } ) { ); } return rects; - }, [ gridItems ] ); + }, [ gridItemStyles ] ); return range( 1, gridInfo.numRows ).map( ( row ) => range( 1, gridInfo.numColumns ).map( ( column ) => { diff --git a/packages/block-editor/src/store/private-selectors.js b/packages/block-editor/src/store/private-selectors.js index dcd315a0ae2804..7f0a4c58ab6747 100644 --- a/packages/block-editor/src/store/private-selectors.js +++ b/packages/block-editor/src/store/private-selectors.js @@ -515,3 +515,24 @@ export function getTemporarilyEditingFocusModeToRevert( state ) { export function getInserterSearchInputRef( state ) { return state.inserterSearchInputRef; } + +/** + * Returns the style attributes of multiple blocks. + * + * @param {Object} state Global application state. + * @param {string[]} clientIds An array of block client IDs. + * + * @return {Object} An object where keys are client IDs and values are the corresponding block styles or undefined. + */ +export const getBlockStyles = createSelector( + ( state, clientIds ) => + clientIds.reduce( ( styles, clientId ) => { + styles[ clientId ] = state.blocks.attributes.get( clientId )?.style; + return styles; + }, {} ), + ( state, clientIds ) => [ + ...clientIds.map( + ( clientId ) => state.blocks.attributes.get( clientId )?.style + ), + ] +); diff --git a/packages/block-editor/src/store/test/private-selectors.js b/packages/block-editor/src/store/test/private-selectors.js index 185da1ffb98046..45432b750bb9eb 100644 --- a/packages/block-editor/src/store/test/private-selectors.js +++ b/packages/block-editor/src/store/test/private-selectors.js @@ -9,6 +9,7 @@ import { getEnabledBlockParents, getExpandedBlock, isDragging, + getBlockStyles, } from '../private-selectors'; import { getBlockEditingMode } from '../selectors'; @@ -509,4 +510,92 @@ describe( 'private selectors', () => { ); } ); } ); + + describe( 'getBlockStyles', () => { + it( 'should return an empty object when no client IDs are provided', () => { + const state = { + blocks: { + attributes: new Map(), + }, + }; + const result = getBlockStyles( state, [] ); + expect( result ).toEqual( {} ); + } ); + + it( 'should return styles for a single block', () => { + const state = { + blocks: { + attributes: new Map( [ + [ 'block-1', { style: { color: 'red' } } ], + ] ), + }, + }; + const result = getBlockStyles( state, [ 'block-1' ] ); + expect( result ).toEqual( { + 'block-1': { color: 'red' }, + } ); + } ); + + it( 'should return styles for multiple blocks', () => { + const state = { + blocks: { + attributes: new Map( [ + [ 'block-1', { style: { color: 'red' } } ], + [ 'block-2', { style: { fontSize: '16px' } } ], + [ 'block-3', { style: { margin: '10px' } } ], + ] ), + }, + }; + const result = getBlockStyles( state, [ + 'block-1', + 'block-2', + 'block-3', + ] ); + expect( result ).toEqual( { + 'block-1': { color: 'red' }, + 'block-2': { fontSize: '16px' }, + 'block-3': { margin: '10px' }, + } ); + } ); + + it( 'should return undefined for blocks without styles', () => { + const state = { + blocks: { + attributes: new Map( [ + [ 'block-1', { style: { color: 'red' } } ], + [ 'block-2', {} ], + [ 'block-3', { style: { margin: '10px' } } ], + ] ), + }, + }; + const result = getBlockStyles( state, [ + 'block-1', + 'block-2', + 'block-3', + ] ); + expect( result ).toEqual( { + 'block-1': { color: 'red' }, + 'block-2': undefined, + 'block-3': { margin: '10px' }, + } ); + } ); + + it( 'should return undefined for non-existent blocks', () => { + const state = { + blocks: { + attributes: new Map( [ + [ 'block-1', { style: { color: 'red' } } ], + ] ), + }, + }; + const result = getBlockStyles( state, [ + 'block-1', + 'non-existent-block', + ] ); + expect( result ).toEqual( { + 'block-1': { color: 'red' }, + 'non-existent-block': undefined, + } ); + } ); + } ); } ); From 1dfcd70549bacd2ec26623d4ecf62559ff40b72c Mon Sep 17 00:00:00 2001 From: Ella <4710635+ellatrix@users.noreply.github.com> Date: Thu, 15 Aug 2024 09:28:46 +0200 Subject: [PATCH 096/126] Paste: fix blob uploading (#64479) --- packages/block-library/src/image/transforms.js | 8 +++++++- packages/block-library/src/video/transforms.js | 6 +++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/block-library/src/image/transforms.js b/packages/block-library/src/image/transforms.js index fda29a2ca2530a..0e1dfb6ee9da44 100644 --- a/packages/block-library/src/image/transforms.js +++ b/packages/block-library/src/image/transforms.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { createBlobURL } from '@wordpress/blob'; +import { createBlobURL, isBlobURL } from '@wordpress/blob'; import { createBlock, getBlockAttributes } from '@wordpress/blocks'; import { dispatch } from '@wordpress/data'; import { store as noticesStore } from '@wordpress/notices'; @@ -123,6 +123,12 @@ const transforms = { anchor, } ); + + if ( isBlobURL( attributes.url ) ) { + attributes.blob = attributes.url; + delete attributes.url; + } + return createBlock( 'core/image', attributes ); }, }, diff --git a/packages/block-library/src/video/transforms.js b/packages/block-library/src/video/transforms.js index c1bbb3431aaf37..4325cd1e7e7e9d 100644 --- a/packages/block-library/src/video/transforms.js +++ b/packages/block-library/src/video/transforms.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { createBlobURL } from '@wordpress/blob'; +import { createBlobURL, isBlobURL } from '@wordpress/blob'; import { createBlock } from '@wordpress/blocks'; const transforms = { @@ -92,6 +92,10 @@ const transforms = { poster: videoElement.getAttribute( 'poster' ) || undefined, src: videoElement.getAttribute( 'src' ) || undefined, }; + if ( isBlobURL( attributes.src ) ) { + attributes.blob = attributes.src; + delete attributes.src; + } return createBlock( 'core/video', attributes ); }, }, From f5f0c67ca561da46fe5430df75c4beffc8b1ce8d Mon Sep 17 00:00:00 2001 From: Dave Smith Date: Thu, 15 Aug 2024 10:04:12 +0100 Subject: [PATCH 097/126] Add private isZoomOutMode selector (#64503) --- packages/block-editor/src/store/private-selectors.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/block-editor/src/store/private-selectors.js b/packages/block-editor/src/store/private-selectors.js index 7f0a4c58ab6747..58479d69cbfbe7 100644 --- a/packages/block-editor/src/store/private-selectors.js +++ b/packages/block-editor/src/store/private-selectors.js @@ -536,3 +536,14 @@ export const getBlockStyles = createSelector( ), ] ); + +/** + * Returns whether zoom out mode is enabled. + * + * @param {Object} state Editor state. + * + * @return {boolean} Is zoom out mode enabled. + */ +export function isZoomOutMode( state ) { + return state.editorMode === 'zoom-out'; +} From 8fc3df806a7e3494937477155fad18f92fdb9538 Mon Sep 17 00:00:00 2001 From: Shail Mehta Date: Thu, 15 Aug 2024 14:53:48 +0530 Subject: [PATCH 098/126] Post Author Name: Add Border Support (#64530) * Post Author Name: Border Support * Post Author Name: Border Support * Keep Spacing Controle bydefault * Added Border Control Display By Default Co-authored-by: shail-mehta Co-authored-by: aaronrobertshaw --- .../block-library/src/post-author-name/block.json | 15 ++++++++++++++- .../block-library/src/post-author-name/style.scss | 4 ++++ packages/block-library/src/style.scss | 1 + 3 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 packages/block-library/src/post-author-name/style.scss diff --git a/packages/block-library/src/post-author-name/block.json b/packages/block-library/src/post-author-name/block.json index 31874ddbf9bc5d..68d2c49bd91056 100644 --- a/packages/block-library/src/post-author-name/block.json +++ b/packages/block-library/src/post-author-name/block.json @@ -53,6 +53,19 @@ }, "interactivity": { "clientNavigation": true + }, + "__experimentalBorder": { + "radius": true, + "color": true, + "width": true, + "style": true, + "__experimentalDefaultControls": { + "radius": true, + "color": true, + "width": true, + "style": true + } } - } + }, + "style": "wp-block-post-author-name" } diff --git a/packages/block-library/src/post-author-name/style.scss b/packages/block-library/src/post-author-name/style.scss new file mode 100644 index 00000000000000..0f57b30490fa68 --- /dev/null +++ b/packages/block-library/src/post-author-name/style.scss @@ -0,0 +1,4 @@ +.wp-block-post-author-name { + // This block has customizable padding, border-box makes that more predictable. + box-sizing: border-box; +} diff --git a/packages/block-library/src/style.scss b/packages/block-library/src/style.scss index 86fe5a3eefabaf..338753ae53fcbf 100644 --- a/packages/block-library/src/style.scss +++ b/packages/block-library/src/style.scss @@ -40,6 +40,7 @@ @import "./post-terms/style.scss"; @import "./post-time-to-read/style.scss"; @import "./post-title/style.scss"; +@import "./post-author-name/style.scss"; @import "./preformatted/style.scss"; @import "./pullquote/style.scss"; @import "./post-template/style.scss"; From 787895eb794432803d45f88c4da9addb7bda0202 Mon Sep 17 00:00:00 2001 From: Shail Mehta Date: Thu, 15 Aug 2024 16:18:23 +0530 Subject: [PATCH 099/126] Updated small typo in compat.php file (#64535) Co-authored-by: shail-mehta Co-authored-by: t-hamano --- lib/compat/wordpress-6.7/compat.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/compat/wordpress-6.7/compat.php b/lib/compat/wordpress-6.7/compat.php index 7021cab2053eff..edc8e3fa5fb03f 100644 --- a/lib/compat/wordpress-6.7/compat.php +++ b/lib/compat/wordpress-6.7/compat.php @@ -64,7 +64,7 @@ function ( $registered_template ) use ( $template_files ) { * Hooks into `get_block_template` to add the `plugin` property when necessary. * * @param [WP_Block_Template|null] $block_template The found block template, or null if there isn’t one. - * @return [WP_Block_Template|null] The block template that was already found with the plugin property defined if it was reigstered by a plugin. + * @return [WP_Block_Template|null] The block template that was already found with the plugin property defined if it was registered by a plugin. */ function _gutenberg_add_block_template_plugin_attribute( $block_template ) { if ( $block_template ) { From cdbd5829e24459f9f2e23132ae46824ec1571c85 Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Thu, 15 Aug 2024 20:10:38 +0900 Subject: [PATCH 100/126] ToggleGroupControl: Add lint rule for 40px size prop usage (#64524) * Fix in blocks * Fix in Navigation block * Fix in Comments Pagination block * Fix in Grid layout * Fix in Flex layout * Fix in Constrained layout (e.g. Group block) * Add lint rule Co-authored-by: mirka <0mirka00@git.wordpress.org> Co-authored-by: ciampo --- .eslintrc.js | 1 + packages/block-editor/src/layouts/constrained.js | 1 + packages/block-editor/src/layouts/flex.js | 2 ++ packages/block-editor/src/layouts/grid.js | 1 + .../comments-pagination-arrow-controls.js | 1 + packages/block-library/src/navigation/edit/index.js | 6 +++--- .../src/navigation/edit/overlay-menu-preview.js | 2 ++ packages/block-library/src/navigation/editor.scss | 5 +++++ .../src/post-featured-image/dimension-controls.js | 1 + packages/block-library/src/post-navigation-link/edit.js | 1 + .../src/query-pagination/query-pagination-arrow-controls.js | 1 + 11 files changed, 19 insertions(+), 3 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index eb2d2db47e4cab..94b443853e0a81 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -315,6 +315,7 @@ module.exports = { 'BorderControl', 'DimensionControl', 'FontSizePicker', + 'ToggleGroupControl', ].map( ( componentName ) => ( { // Falsy `__next40pxDefaultSize` without a non-default `size` prop. selector: `JSXOpeningElement[name.name="${ componentName }"]:not(:has(JSXAttribute[name.name="__next40pxDefaultSize"][value.expression.value!=false])):not(:has(JSXAttribute[name.name="size"][value.value!="default"]))`, diff --git a/packages/block-editor/src/layouts/constrained.js b/packages/block-editor/src/layouts/constrained.js index 2e671e8e53975a..b48e331792bd8a 100644 --- a/packages/block-editor/src/layouts/constrained.js +++ b/packages/block-editor/src/layouts/constrained.js @@ -125,6 +125,7 @@ export default { ) } { allowJustification && ( ) } -

{ __( 'Overlay Menu' ) }

setAttributes( { icon: value } ) } diff --git a/packages/block-library/src/navigation/editor.scss b/packages/block-library/src/navigation/editor.scss index b279dc08cfe6e7..24480bb1592516 100644 --- a/packages/block-library/src/navigation/editor.scss +++ b/packages/block-library/src/navigation/editor.scss @@ -628,3 +628,8 @@ body.editor-styles-wrapper .wp-block-navigation__responsive-container.is-menu-op .wp-block-navigation__menu-inspector-controls__empty-message { margin-left: 24px; } + +.wp-block-navigation__overlay-menu-icon-toggle-group { + // Counteract the margin added by the block inspector. + margin-bottom: $grid-unit-20; +} diff --git a/packages/block-library/src/post-featured-image/dimension-controls.js b/packages/block-library/src/post-featured-image/dimension-controls.js index c8e8c0005cfef5..4d272d3d8bb415 100644 --- a/packages/block-library/src/post-featured-image/dimension-controls.js +++ b/packages/block-library/src/post-featured-image/dimension-controls.js @@ -203,6 +203,7 @@ const DimensionControls = ( { panelId={ clientId } > ) } Date: Thu, 15 Aug 2024 14:01:48 +0200 Subject: [PATCH 101/126] Fix: Changing sorting direction on patterns does nothing. (#64508) Co-authored-by: jorgefilipecosta Co-authored-by: t-hamano --- .../src/components/dataviews-view-config/index.tsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/dataviews/src/components/dataviews-view-config/index.tsx b/packages/dataviews/src/components/dataviews-view-config/index.tsx index 0aae714c5ce27a..69b4cba7763262 100644 --- a/packages/dataviews/src/components/dataviews-view-config/index.tsx +++ b/packages/dataviews/src/components/dataviews-view-config/index.tsx @@ -140,7 +140,7 @@ function SortFieldControl() { } function SortDirectionControl() { - const { view, onChangeView } = useContext( DataViewsContext ); + const { view, fields, onChangeView } = useContext( DataViewsContext ); return ( { - if ( ! view?.sort?.field ) { - return; - } if ( newDirection === 'asc' || newDirection === 'desc' ) { onChangeView( { ...view, sort: { direction: newDirection, - field: view.sort.field, + field: + view.sort?.field || + // If there is no field assigned as the sorting field assign the first sortable field. + fields.find( + ( field ) => field.enableSorting !== false + )?.id || + '', }, } ); return; From cb224d4a5a73ebe3d616d54d72a1590dbbdfc09f Mon Sep 17 00:00:00 2001 From: Aki Hamano <54422211+t-hamano@users.noreply.github.com> Date: Thu, 15 Aug 2024 21:43:35 +0900 Subject: [PATCH 102/126] Quote Block: Add align support (#64188) * Quote Block: Add align support * Inherit alignment when transforming from pullquote block * Inherit alignment when transforming to pullquote block * Try to fix mobile unit test * Try to fix mobile unit test * Try to fix mobile unit test Co-authored-by: t-hamano Co-authored-by: ramonjd Co-authored-by: geriux --- docs/reference-guides/core-blocks.md | 2 +- packages/block-library/src/quote/block.json | 1 + .../src/quote/test/transforms.native.js | 2 +- packages/block-library/src/quote/transforms.js | 13 +++++++++++-- .../src/mobile/utils/alignments.native.js | 1 + 5 files changed, 15 insertions(+), 4 deletions(-) diff --git a/docs/reference-guides/core-blocks.md b/docs/reference-guides/core-blocks.md index 5beb712c80a113..b9cae44550181c 100644 --- a/docs/reference-guides/core-blocks.md +++ b/docs/reference-guides/core-blocks.md @@ -783,7 +783,7 @@ Give quoted text visual emphasis. "In quoting others, we cite ourselves." — Ju - **Name:** core/quote - **Category:** text -- **Supports:** anchor, background (backgroundImage, backgroundSize), color (background, gradients, heading, link, text), dimensions (minHeight), interactivity (clientNavigation), layout (~~allowEditing~~), spacing (blockGap, margin, padding), typography (fontSize, lineHeight), ~~html~~ +- **Supports:** align (full, left, right, wide), anchor, background (backgroundImage, backgroundSize), color (background, gradients, heading, link, text), dimensions (minHeight), interactivity (clientNavigation), layout (~~allowEditing~~), spacing (blockGap, margin, padding), typography (fontSize, lineHeight), ~~html~~ - **Attributes:** citation, textAlign, value ## Read More diff --git a/packages/block-library/src/quote/block.json b/packages/block-library/src/quote/block.json index c695c938923ae7..0f9ec97422f64b 100644 --- a/packages/block-library/src/quote/block.json +++ b/packages/block-library/src/quote/block.json @@ -28,6 +28,7 @@ }, "supports": { "anchor": true, + "align": [ "left", "right", "wide", "full" ], "html": false, "background": { "backgroundImage": true, diff --git a/packages/block-library/src/quote/test/transforms.native.js b/packages/block-library/src/quote/test/transforms.native.js index 25030e0a018d41..3d2bc8ae609182 100644 --- a/packages/block-library/src/quote/test/transforms.native.js +++ b/packages/block-library/src/quote/test/transforms.native.js @@ -14,7 +14,7 @@ import { const block = 'Quote'; const initialHtml = ` - +

"This will make running your own blog a viable alternative again."

Adrian Zumbrunnen
diff --git a/packages/block-library/src/quote/transforms.js b/packages/block-library/src/quote/transforms.js index f9b3970433fad6..c960759691bf16 100644 --- a/packages/block-library/src/quote/transforms.js +++ b/packages/block-library/src/quote/transforms.js @@ -9,10 +9,18 @@ const transforms = { { type: 'block', blocks: [ 'core/pullquote' ], - transform: ( { value, citation, anchor, fontSize, style } ) => { + transform: ( { + value, + align, + citation, + anchor, + fontSize, + style, + } ) => { return createBlock( 'core/quote', { + align, citation, anchor, fontSize, @@ -95,7 +103,7 @@ const transforms = { ); }, transform: ( - { citation, anchor, fontSize, style }, + { align, citation, anchor, fontSize, style }, innerBlocks ) => { const value = innerBlocks @@ -103,6 +111,7 @@ const transforms = { .join( '
' ); return createBlock( 'core/pullquote', { value, + align, citation, anchor, fontSize, diff --git a/packages/components/src/mobile/utils/alignments.native.js b/packages/components/src/mobile/utils/alignments.native.js index bc42385988a5d1..f1f737d7ed367a 100644 --- a/packages/components/src/mobile/utils/alignments.native.js +++ b/packages/components/src/mobile/utils/alignments.native.js @@ -13,6 +13,7 @@ export const WIDE_ALIGNMENTS = { 'core/image', 'core/separator', 'core/media-text', + 'core/quote', 'core/pullquote', ], }; From d530eca033c91cacbd047eda685c45af787913b4 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Thu, 15 Aug 2024 16:11:22 +0200 Subject: [PATCH 103/126] Update: Move item size control to the new view config UI. (#64380) Co-authored-by: jorgefilipecosta Co-authored-by: jameskoster Co-authored-by: t-hamano Co-authored-by: jasmussen Co-authored-by: tyxla Co-authored-by: mirka <0mirka00@git.wordpress.org> --- .../dataviews-view-config/index.tsx | 40 +++++++-- .../src/components/dataviews/index.tsx | 10 +-- .../dataviews-layouts/grid/density-picker.tsx | 84 ++++--------------- .../src/dataviews-layouts/grid/style.scss | 4 - 4 files changed, 52 insertions(+), 86 deletions(-) diff --git a/packages/dataviews/src/components/dataviews-view-config/index.tsx b/packages/dataviews/src/components/dataviews-view-config/index.tsx index 69b4cba7763262..034f618aea00ff 100644 --- a/packages/dataviews/src/components/dataviews-view-config/index.tsx +++ b/packages/dataviews/src/components/dataviews-view-config/index.tsx @@ -30,11 +30,17 @@ import warning from '@wordpress/warning'; /** * Internal dependencies */ -import { SORTING_DIRECTIONS, sortIcons, sortLabels } from '../../constants'; +import { + SORTING_DIRECTIONS, + LAYOUT_GRID, + sortIcons, + sortLabels, +} from '../../constants'; import { VIEW_LAYOUTS, getMandatoryFields } from '../../dataviews-layouts'; import type { SupportedLayouts } from '../../types'; import DataViewsContext from '../dataviews-context'; import { unlock } from '../../lock-unlock'; +import DensityPicker from '../../dataviews-layouts/grid/density-picker'; const { DropdownMenuV2: DropdownMenu, @@ -101,10 +107,6 @@ function ViewTypeMenu( { ); } -interface ViewActionsProps { - defaultLayouts?: SupportedLayouts; -} - function SortFieldControl() { const { view, fields, onChangeView } = useContext( DataViewsContext ); const orderOptions = useMemo( () => { @@ -305,7 +307,14 @@ function SettingsSection( { ); } -function DataviewsViewConfigContent() { +function DataviewsViewConfigContent( { + density, + setDensity, +}: { + density: number; + setDensity: React.Dispatch< React.SetStateAction< number > >; +} ) { + const { view } = useContext( DataViewsContext ); return ( @@ -313,6 +322,12 @@ function DataviewsViewConfigContent() { + { view.type === LAYOUT_GRID && ( + + ) } @@ -323,8 +338,14 @@ function DataviewsViewConfigContent() { } function _DataViewsViewConfig( { + density, + setDensity, defaultLayouts = { list: {}, grid: {}, table: {} }, -}: ViewActionsProps ) { +}: { + density: number; + setDensity: React.Dispatch< React.SetStateAction< number > >; + defaultLayouts?: SupportedLayouts; +} ) { const [ isShowingViewPopover, setIsShowingViewPopover ] = useState< boolean >( false ); @@ -346,7 +367,10 @@ function _DataViewsViewConfig( { } } focusOnMount > - + ) } diff --git a/packages/dataviews/src/components/dataviews/index.tsx b/packages/dataviews/src/components/dataviews/index.tsx index 337912b04e59c5..81f901f0859bbc 100644 --- a/packages/dataviews/src/components/dataviews/index.tsx +++ b/packages/dataviews/src/components/dataviews/index.tsx @@ -27,8 +27,6 @@ import DataViewsViewConfig from '../dataviews-view-config'; import { normalizeFields } from '../../normalize-fields'; import type { Action, Field, View, SupportedLayouts } from '../../types'; import type { SelectionOrUpdater } from '../../private-types'; -import DensityPicker from '../../dataviews-layouts/grid/density-picker'; -import { LAYOUT_GRID } from '../../constants'; type ItemWithId = { id: string }; @@ -133,12 +131,6 @@ export default function DataViews< Item >( { isShowingFilter={ isShowingFilter } /> - { view.type === LAYOUT_GRID && ( - - ) } ( { > { header } diff --git a/packages/dataviews/src/dataviews-layouts/grid/density-picker.tsx b/packages/dataviews/src/dataviews-layouts/grid/density-picker.tsx index 364d764e343470..8f0782878af224 100644 --- a/packages/dataviews/src/dataviews-layouts/grid/density-picker.tsx +++ b/packages/dataviews/src/dataviews-layouts/grid/density-picker.tsx @@ -1,10 +1,9 @@ /** * WordPress dependencies */ -import { RangeControl, Button } from '@wordpress/components'; +import { RangeControl } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { useViewportMatch } from '@wordpress/compose'; -import { plus, reset } from '@wordpress/icons'; import { useEffect } from '@wordpress/element'; const viewportBreaks = { @@ -40,21 +39,6 @@ function useViewPortBreakpoint() { return null; } -// Value is number from 0 to 100 representing how big an item is in the grid -// 100 being the biggest and 0 being the smallest. -// The size is relative to the viewport size, if one a given viewport the -// number of allowed items in a grid is 3 to 6 a 0 ( the smallest ) will mean that the grid will -// have 6 items in a row, a 100 ( the biggest ) will mean that the grid will have 3 items in a row. -// A value of 75 will mean that the grid will have 4 items in a row. -function getRangeValue( - density: number, - breakValues: { min: number; max: number; default: number } -) { - const inverseDensity = breakValues.max - density; - const max = breakValues.max - breakValues.min; - return Math.round( ( inverseDensity * 100 ) / max ); -} - export default function DensityPicker( { density, setDensity, @@ -78,59 +62,27 @@ export default function DensityPicker( { return _density; } ); }, [ setDensity, viewport ] ); + const breakValues = viewportBreaks[ viewport || 'mobile' ]; + const densityToUse = density || breakValues.default; + if ( ! viewport ) { return null; } - const breakValues = viewportBreaks[ viewport ]; - const densityToUse = density || breakValues.default; - const rangeValue = getRangeValue( densityToUse, breakValues ); - const step = 100 / ( breakValues.max - breakValues.min + 1 ); return ( - <> - + ); +} + const ProductScreen = ( { onBackButtonClick, }: { @@ -344,20 +365,20 @@ const MyHierarchicalNavigation = ( { > { BUTTON_TEXT.toNestedScreen } - { BUTTON_TEXT.back } - +

{ SCREEN_TEXT.nested }

- { BUTTON_TEXT.back } - + { + return ( + <> + + +

{ SCREEN_TEXT.home }

+ { /* + * A button useful to test focus restoration. This button is the first + * tabbable item in the screen, but should not receive focus when + * navigating to screen as a result of a backwards navigation. + */ } + + + { BUTTON_TEXT.toChildScreen } + +
+ + +

{ SCREEN_TEXT.child }

+ { /* + * A button useful to test focus restoration. This button is the first + * tabbable item in the screen, but should not receive focus when + * navigating to screen as a result of a backwards navigation. + */ } + + + { BUTTON_TEXT.toNestedScreen } + + + { BUTTON_TEXT.back } + +
+ + +

{ SCREEN_TEXT.nested }

+ + { BUTTON_TEXT.back } + +
+
+ + ); +}; + const getScreen = ( screenKey: keyof typeof SCREEN_TEXT ) => screen.getByText( SCREEN_TEXT[ screenKey ] ); const queryScreen = ( screenKey: keyof typeof SCREEN_TEXT ) => @@ -769,4 +850,53 @@ describe( 'Navigator', () => { ).toHaveFocus(); } ); } ); + + describe( 'deprecated APIs', () => { + it( 'should log a deprecation notice when using the NavigatorToParentButton component', async () => { + const user = userEvent.setup(); + + render( ); + + expect( getScreen( 'child' ) ).toBeInTheDocument(); + + // Navigate back to home screen. + // The first tabbable element receives focus, since focus restoration + // it not possible (there was no forward navigation). + await user.click( getNavigationButton( 'back' ) ); + expect( getScreen( 'home' ) ).toBeInTheDocument(); + expect( + screen.getByRole( 'button', { + name: 'First tabbable home screen button', + } ) + ).toHaveFocus(); + + // Rendering `NavigatorToParentButton` logs a deprecation notice + expect( console ).toHaveWarnedWith( + 'wp.components.NavigatorToParentButton is deprecated since version 6.7. Please use wp.components.NavigatorBackButton instead.' + ); + } ); + + it( 'should log a deprecation notice when using the useNavigator().goToParent() function', async () => { + const user = userEvent.setup(); + + render( ); + + expect( getScreen( 'nested' ) ).toBeInTheDocument(); + + // Navigate back to child screen using the back button. + // The first tabbable element receives focus, since focus restoration + // it not possible (there was no forward navigation). + await user.click( getNavigationButton( 'back' ) ); + expect( getScreen( 'child' ) ).toBeInTheDocument(); + expect( + screen.getByRole( 'button', { + name: 'First tabbable child screen button', + } ) + ).toHaveFocus(); + + expect( console ).toHaveWarnedWith( + 'wp.components.useNavigator().goToParent is deprecated since version 6.7. Please use wp.components.useNavigator().goBack instead.' + ); + } ); + } ); } ); diff --git a/packages/components/src/navigator/types.ts b/packages/components/src/navigator/types.ts index 557f8074fd42e2..c45762d558af2d 100644 --- a/packages/components/src/navigator/types.ts +++ b/packages/components/src/navigator/types.ts @@ -11,26 +11,70 @@ import type { ButtonAsButtonProps } from '../button/types'; export type MatchParams = Record< string, string | string[] >; export type NavigateOptions = { + /** + * Specify the CSS selector used to restore focus on an given element when + * navigating back. When not provided, the component will attempt to restore + * focus on the element that originated the forward navigation. + */ focusTargetSelector?: string; + /** + * Whether the navigation is a backwards navigation. This enables focus + * restoration (when possible), and causes the animation to be backwards. + */ isBack?: boolean; + /** + * Opt out of focus management. Useful when the consumer of the component + * wants to manage focus themselves. + */ skipFocus?: boolean; + /** + * Whether the navigation should replace the current location in the stack. + */ replace?: boolean; }; export type NavigateToParentOptions = Omit< NavigateOptions, 'isBack' >; export type NavigatorLocation = NavigateOptions & { + /** + * Whether the current location is the initial one (ie. first in the stack). + */ isInitial?: boolean; + /** + * The path associated to the location. + */ path?: string; + /** + * Whether focus was already restored for this location (in case of + * backwards navigation). + */ hasRestoredFocus?: boolean; }; // Returned by the `useNavigator` hook. export type Navigator = { + /** + * The current location. + */ location: NavigatorLocation; + /** + * Params associated with the current location + */ params: MatchParams; + /** + * Navigate to a new location. + */ goTo: ( path: string, options?: NavigateOptions ) => void; - goBack: () => void; + /** + * Go back to the parent location (ie. "/some/path" will navigate back + * to "/some") + */ + goBack: ( options?: NavigateToParentOptions ) => void; + /** + * _Note: This function is deprecated. Please use `goBack` instead._ + * @deprecated + * @ignore + */ goToParent: ( options?: NavigateToParentOptions ) => void; }; @@ -64,15 +108,6 @@ export type NavigatorScreenProps = { export type NavigatorBackButtonProps = ButtonAsButtonProps; -export type NavigatorBackButtonHookProps = NavigatorBackButtonProps & { - /** - * Whether we should navigate to the parent screen. - * - * @default 'false' - */ - goToParent?: boolean; -}; - export type NavigatorToParentButtonProps = NavigatorBackButtonProps; export type NavigatorButtonProps = NavigatorBackButtonProps & { From 65da2128fb798638717bc27d55823590afc3cd47 Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Sat, 17 Aug 2024 01:56:12 +0900 Subject: [PATCH 121/126] CustomSelectControl: Add lint rule for 40px size prop usage (#64559) * Fix in DateFormatPicker * Make explicit in FontAppearanceControl * Add lint rule Co-authored-by: mirka <0mirka00@git.wordpress.org> Co-authored-by: tyxla Co-authored-by: ciampo --- .eslintrc.js | 1 + .../block-editor/src/components/date-format-picker/index.js | 1 + .../src/components/font-appearance-control/index.js | 3 +++ 3 files changed, 5 insertions(+) diff --git a/.eslintrc.js b/.eslintrc.js index 2cdbb5c056cb14..42bb0accd72f1a 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -313,6 +313,7 @@ module.exports = { ...[ 'BorderBoxControl', 'BorderControl', + 'CustomSelectControl', 'DimensionControl', 'FontSizePicker', 'ToggleGroupControl', diff --git a/packages/block-editor/src/components/date-format-picker/index.js b/packages/block-editor/src/components/date-format-picker/index.js index 63c977b111e01c..edefd6249f1aae 100644 --- a/packages/block-editor/src/components/date-format-picker/index.js +++ b/packages/block-editor/src/components/date-format-picker/index.js @@ -129,6 +129,7 @@ function NonDefaultControls( { format, onChange } ) { return ( { */ export default function FontAppearanceControl( props ) { const { + /** Start opting into the larger default height that will become the default size in a future version. */ + __next40pxDefaultSize = false, onChange, hasFontStyles = true, hasFontWeights = true, @@ -150,6 +152,7 @@ export default function FontAppearanceControl( props ) { Date: Sat, 17 Aug 2024 02:38:24 +0900 Subject: [PATCH 122/126] NumberControl: Add lint rule for 40px size prop usage (#64561) Co-authored-by: mirka <0mirka00@git.wordpress.org> Co-authored-by: ciampo Co-authored-by: tyxla --- .eslintrc.js | 1 + .../src/components/line-height-control/README.md | 7 +++++++ .../src/components/line-height-control/index.js | 3 +++ 3 files changed, 11 insertions(+) diff --git a/.eslintrc.js b/.eslintrc.js index 42bb0accd72f1a..e21f7d6e6d7806 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -316,6 +316,7 @@ module.exports = { 'CustomSelectControl', 'DimensionControl', 'FontSizePicker', + 'NumberControl', 'ToggleGroupControl', ].map( ( componentName ) => ( { // Falsy `__next40pxDefaultSize` without a non-default `size` prop. diff --git a/packages/block-editor/src/components/line-height-control/README.md b/packages/block-editor/src/components/line-height-control/README.md index dafad9145022b9..89bcc69622367f 100644 --- a/packages/block-editor/src/components/line-height-control/README.md +++ b/packages/block-editor/src/components/line-height-control/README.md @@ -36,6 +36,13 @@ The value of the line height. A callback function that handles the application of the line height value. +#### `__next40pxDefaultSize` + +- **Type:** `boolean` +- **Default:** `false` + +Start opting into the larger default height that will become the default size in a future version. + ## Related components Block Editor components are components that can be used to compose the UI of your block editor. Thus, they can only be used under a [`BlockEditorProvider`](https://github.com/WordPress/gutenberg/blob/HEAD/packages/block-editor/src/components/provider/README.md) in the components tree. diff --git a/packages/block-editor/src/components/line-height-control/index.js b/packages/block-editor/src/components/line-height-control/index.js index d605aea3d2ef18..b2c99c03f87840 100644 --- a/packages/block-editor/src/components/line-height-control/index.js +++ b/packages/block-editor/src/components/line-height-control/index.js @@ -16,6 +16,8 @@ import { } from './utils'; const LineHeightControl = ( { + /** Start opting into the larger default height that will become the default size in a future version. */ + __next40pxDefaultSize = false, value: lineHeight, onChange, __unstableInputWidth = '60px', @@ -91,6 +93,7 @@ const LineHeightControl = ( {
Date: Sat, 17 Aug 2024 04:01:35 +0900 Subject: [PATCH 123/126] ComboboxControl: Add lint rule for 40px size prop usage (#64560) * Fix in Author block * Fix in Avatar block * Add lint rule Co-authored-by: mirka <0mirka00@git.wordpress.org> Co-authored-by: ciampo Co-authored-by: tyxla --- .eslintrc.js | 1 + .../block-library/src/avatar/user-control.js | 1 + packages/block-library/src/editor.scss | 1 + .../block-library/src/post-author/block.json | 1 + .../block-library/src/post-author/edit.js | 125 ++++++++++-------- .../block-library/src/post-author/editor.scss | 7 + 6 files changed, 78 insertions(+), 58 deletions(-) create mode 100644 packages/block-library/src/post-author/editor.scss diff --git a/.eslintrc.js b/.eslintrc.js index e21f7d6e6d7806..f6b24dcb15017f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -313,6 +313,7 @@ module.exports = { ...[ 'BorderBoxControl', 'BorderControl', + 'ComboboxControl', 'CustomSelectControl', 'DimensionControl', 'FontSizePicker', diff --git a/packages/block-library/src/avatar/user-control.js b/packages/block-library/src/avatar/user-control.js index 598c05011eaed7..235202ffec90d7 100644 --- a/packages/block-library/src/avatar/user-control.js +++ b/packages/block-library/src/avatar/user-control.js @@ -33,6 +33,7 @@ function UserControl( { value, onChange } ) { return ( - { showAuthorControl && - ( ( showCombobox && ( - - ) ) || ( + + { showAuthorControl && + ( ( showCombobox && ( + + ) ) || ( + + ) ) } + + setAttributes( { showAvatar: ! showAvatar } ) + } + /> + { showAvatar && ( { + setAttributes( { + avatarSize: Number( size ), + } ); + } } /> - ) ) } - - setAttributes( { showAvatar: ! showAvatar } ) - } - /> - { showAvatar && ( - { - setAttributes( { - avatarSize: Number( size ), - } ); - } } + label={ __( 'Show bio' ) } + checked={ showBio } + onChange={ () => + setAttributes( { showBio: ! showBio } ) + } /> - ) } - - setAttributes( { showBio: ! showBio } ) - } - /> - setAttributes( { isLink: ! isLink } ) } - /> - { isLink && ( - setAttributes( { - linkTarget: value ? '_blank' : '_self', - } ) + label={ __( 'Link author name to author page' ) } + checked={ isLink } + onChange={ () => + setAttributes( { isLink: ! isLink } ) } - checked={ linkTarget === '_blank' } /> - ) } + { isLink && ( + + setAttributes( { + linkTarget: value ? '_blank' : '_self', + } ) + } + checked={ linkTarget === '_blank' } + /> + ) } + diff --git a/packages/block-library/src/post-author/editor.scss b/packages/block-library/src/post-author/editor.scss new file mode 100644 index 00000000000000..f6464893138cff --- /dev/null +++ b/packages/block-library/src/post-author/editor.scss @@ -0,0 +1,7 @@ +.wp-block-post-author__inspector-settings { + // Counteract the margin added by the block inspector. + .components-base-control, + .components-base-control:last-child { + margin-bottom: 0; + } +} From c459a7bebec8d8c7db8af2a0be6517a02d573b3c Mon Sep 17 00:00:00 2001 From: Hit Bhalodia <58802366+hbhalodia@users.noreply.github.com> Date: Sat, 17 Aug 2024 00:39:54 +0530 Subject: [PATCH 124/126] Feat: Button groups in Typography tools should use ToggleGroupControl (#64529) * Update the letterCase typogrpahy panel to use toggleGroupOption component * Remove unecessary style for segmented text control and match height with components * Remove SegmentedTextControl component and use toggleControl * Reduce the diff change in the PR Co-authored-by: hbhalodia Co-authored-by: t-hamano Co-authored-by: mirka <0mirka00@git.wordpress.org> Co-authored-by: ciampo Co-authored-by: tyxla --- .../segmented-text-control/index.js | 63 ------------------- .../segmented-text-control/style.scss | 15 ----- .../text-alignment-control/index.js | 28 ++++++--- .../text-decoration-control/index.js | 28 ++++++--- .../text-transform-control/index.js | 28 ++++++--- .../components/writing-mode-control/index.js | 28 ++++++--- packages/block-editor/src/style.scss | 1 - 7 files changed, 80 insertions(+), 111 deletions(-) delete mode 100644 packages/block-editor/src/components/segmented-text-control/index.js delete mode 100644 packages/block-editor/src/components/segmented-text-control/style.scss diff --git a/packages/block-editor/src/components/segmented-text-control/index.js b/packages/block-editor/src/components/segmented-text-control/index.js deleted file mode 100644 index 28f049bfb40ec9..00000000000000 --- a/packages/block-editor/src/components/segmented-text-control/index.js +++ /dev/null @@ -1,63 +0,0 @@ -/** - * External dependencies - */ -import clsx from 'clsx'; - -/** - * WordPress dependencies - */ -import { BaseControl, Button } from '@wordpress/components'; - -/** - * @typedef {Object} Option - * @property {string} label The label of the option. - * @property {string} value The value of the option. - * @property {string} icon The icon of the option. - */ - -/** - * Control to facilitate selecting a text style from a set of options. - * - * @param {Object} props Component props. - * @param {string} props.label A label for the option. - * @param {string} props.value Currently selected value. - * @param {Function} props.onChange Callback to handle onChange. - * @param {Option[]} props.options Array of options to display. - * @param {string} props.className Additional class name to apply. - * - * @return {Element} Element to render. - */ -export default function SegmentedTextControl( { - label, - value, - options, - onChange, - className, -} ) { - return ( -
- - { label } - -
- { options.map( ( option ) => { - return ( -
-
- ); -} diff --git a/packages/block-editor/src/components/segmented-text-control/style.scss b/packages/block-editor/src/components/segmented-text-control/style.scss deleted file mode 100644 index 7a4a3bbea7cb33..00000000000000 --- a/packages/block-editor/src/components/segmented-text-control/style.scss +++ /dev/null @@ -1,15 +0,0 @@ -.block-editor-segmented-text-control { - border: 0; - margin: 0; - padding: 0; - - .block-editor-segmented-text-control__buttons { - // 4px of padding makes the row 40px high, same as an input. - padding: $grid-unit-05 0; - display: flex; - } - - .components-button.has-icon { - margin-right: $grid-unit-05; - } -} diff --git a/packages/block-editor/src/components/text-alignment-control/index.js b/packages/block-editor/src/components/text-alignment-control/index.js index 88a6fe274ea09b..6eeaad784db0f0 100644 --- a/packages/block-editor/src/components/text-alignment-control/index.js +++ b/packages/block-editor/src/components/text-alignment-control/index.js @@ -14,11 +14,10 @@ import { alignJustify, } from '@wordpress/icons'; import { useMemo } from '@wordpress/element'; - -/** - * Internal dependencies - */ -import SegmentedTextControl from '../segmented-text-control'; +import { + __experimentalToggleGroupControl as ToggleGroupControl, + __experimentalToggleGroupControlOptionIcon as ToggleGroupControlOptionIcon, +} from '@wordpress/components'; const TEXT_ALIGNMENT_OPTIONS = [ { @@ -75,9 +74,11 @@ export default function TextAlignmentControl( { } return ( - { onChange( newValue === value ? undefined : newValue ); } } - /> + > + { validOptions.map( ( option ) => { + return ( + + ); + } ) } + ); } diff --git a/packages/block-editor/src/components/text-decoration-control/index.js b/packages/block-editor/src/components/text-decoration-control/index.js index d06632afdbb3ca..720f3d2d9558eb 100644 --- a/packages/block-editor/src/components/text-decoration-control/index.js +++ b/packages/block-editor/src/components/text-decoration-control/index.js @@ -8,11 +8,10 @@ import clsx from 'clsx'; */ import { reset, formatStrikethrough, formatUnderline } from '@wordpress/icons'; import { __ } from '@wordpress/i18n'; - -/** - * Internal dependencies - */ -import SegmentedTextControl from '../segmented-text-control'; +import { + __experimentalToggleGroupControl as ToggleGroupControl, + __experimentalToggleGroupControlOptionIcon as ToggleGroupControlOptionIcon, +} from '@wordpress/components'; const TEXT_DECORATIONS = [ { @@ -48,9 +47,11 @@ export default function TextDecorationControl( { className, } ) { return ( - { onChange( newValue === value ? undefined : newValue ); } } - /> + > + { TEXT_DECORATIONS.map( ( option ) => { + return ( + + ); + } ) } + ); } diff --git a/packages/block-editor/src/components/text-transform-control/index.js b/packages/block-editor/src/components/text-transform-control/index.js index f448a55ed946c3..a3183e630c328a 100644 --- a/packages/block-editor/src/components/text-transform-control/index.js +++ b/packages/block-editor/src/components/text-transform-control/index.js @@ -13,11 +13,10 @@ import { formatLowercase, formatUppercase, } from '@wordpress/icons'; - -/** - * Internal dependencies - */ -import SegmentedTextControl from '../segmented-text-control'; +import { + __experimentalToggleGroupControl as ToggleGroupControl, + __experimentalToggleGroupControlOptionIcon as ToggleGroupControlOptionIcon, +} from '@wordpress/components'; const TEXT_TRANSFORMS = [ { @@ -54,9 +53,11 @@ const TEXT_TRANSFORMS = [ */ export default function TextTransformControl( { className, value, onChange } ) { return ( - { onChange( newValue === value ? undefined : newValue ); } } - /> + > + { TEXT_TRANSFORMS.map( ( option ) => { + return ( + + ); + } ) } + ); } diff --git a/packages/block-editor/src/components/writing-mode-control/index.js b/packages/block-editor/src/components/writing-mode-control/index.js index 7732f54b1569a7..bffccfd579c6b0 100644 --- a/packages/block-editor/src/components/writing-mode-control/index.js +++ b/packages/block-editor/src/components/writing-mode-control/index.js @@ -8,11 +8,10 @@ import clsx from 'clsx'; */ import { __, isRTL } from '@wordpress/i18n'; import { textHorizontal, textVertical } from '@wordpress/icons'; - -/** - * Internal dependencies - */ -import SegmentedTextControl from '../segmented-text-control'; +import { + __experimentalToggleGroupControl as ToggleGroupControl, + __experimentalToggleGroupControlOptionIcon as ToggleGroupControlOptionIcon, +} from '@wordpress/components'; const WRITING_MODES = [ { @@ -39,14 +38,27 @@ const WRITING_MODES = [ */ export default function WritingModeControl( { className, value, onChange } ) { return ( - { onChange( newValue === value ? undefined : newValue ); } } - /> + > + { WRITING_MODES.map( ( option ) => { + return ( + + ); + } ) } + ); } diff --git a/packages/block-editor/src/style.scss b/packages/block-editor/src/style.scss index 47b87bb50918df..feaabbbda94426 100644 --- a/packages/block-editor/src/style.scss +++ b/packages/block-editor/src/style.scss @@ -40,7 +40,6 @@ @import "./components/multi-selection-inspector/style.scss"; @import "./components/responsive-block-control/style.scss"; @import "./components/rich-text/style.scss"; -@import "./components/segmented-text-control/style.scss"; @import "./components/skip-to-selected-block/style.scss"; @import "./components/tabbed-sidebar/style.scss"; @import "./components/tool-selector/style.scss"; From 68dd5ce54901a2d5f8bf25ba170d603701426ad4 Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Sat, 17 Aug 2024 04:33:24 +0900 Subject: [PATCH 125/126] RangeControl: Add lint rule for 40px size prop usage (#64558) * Fix in Query Loop block * Fix in Excerpt block * Fix in Grid block * Fix in SpacingSizesControl * Fix in Image block Zoom control * Fix in HeightControl * Fix in BorderRadiusControl * Add lint rule * Fixup in HeightControl * Address in ColorPicker Co-authored-by: mirka <0mirka00@git.wordpress.org> Co-authored-by: tyxla --- .eslintrc.js | 1 + .../components/border-radius-control/index.js | 1 + .../border-radius-control/style.scss | 10 ------- .../src/components/height-control/index.js | 1 + .../components/image-editor/zoom-dropdown.js | 26 ++++++++++++------- .../input-controls/spacing-input-control.js | 2 ++ .../spacing-sizes-control/style.scss | 10 ------- packages/block-editor/src/layouts/grid.js | 2 ++ .../block-library/src/post-excerpt/edit.js | 1 + .../src/query-pagination-numbers/edit.js | 1 + .../src/color-picker/input-with-slider.tsx | 1 + 11 files changed, 27 insertions(+), 29 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index f6b24dcb15017f..5a939aeb9173b7 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -318,6 +318,7 @@ module.exports = { 'DimensionControl', 'FontSizePicker', 'NumberControl', + 'RangeControl', 'ToggleGroupControl', ].map( ( componentName ) => ( { // Falsy `__next40pxDefaultSize` without a non-default `size` prop. diff --git a/packages/block-editor/src/components/border-radius-control/index.js b/packages/block-editor/src/components/border-radius-control/index.js index 4c614084a7e200..cab9b87b3b29c0 100644 --- a/packages/block-editor/src/components/border-radius-control/index.js +++ b/packages/block-editor/src/components/border-radius-control/index.js @@ -104,6 +104,7 @@ export default function BorderRadiusControl( { onChange, values } ) { units={ units } /> div { - height: 40px; - display: flex; - align-items: center; - } - } - - > span { - flex: 0 0 auto; } } diff --git a/packages/block-editor/src/components/height-control/index.js b/packages/block-editor/src/components/height-control/index.js index 71753a67beb021..5d42e217776d6a 100644 --- a/packages/block-editor/src/components/height-control/index.js +++ b/packages/block-editor/src/components/height-control/index.js @@ -164,6 +164,7 @@ export default function HeightControl( { ) } renderContent={ () => ( - + + + ) } /> ); diff --git a/packages/block-editor/src/components/spacing-sizes-control/input-controls/spacing-input-control.js b/packages/block-editor/src/components/spacing-sizes-control/input-controls/spacing-input-control.js index 5cdfe47c452b23..d00feed704d17a 100644 --- a/packages/block-editor/src/components/spacing-sizes-control/input-controls/spacing-input-control.js +++ b/packages/block-editor/src/components/spacing-sizes-control/input-controls/spacing-input-control.js @@ -249,6 +249,7 @@ export default function SpacingInputControl( { } } /> .components-base-control__field { - /* Fixes RangeControl contents when the outer wrapper is flex */ - flex: 1; - } } .components-range-control__mark { diff --git a/packages/block-editor/src/layouts/grid.js b/packages/block-editor/src/layouts/grid.js index c574ad0ae43d5d..7ab5c7ebbc8c3d 100644 --- a/packages/block-editor/src/layouts/grid.js +++ b/packages/block-editor/src/layouts/grid.js @@ -264,6 +264,7 @@ function GridLayoutMinimumWidthControl( { layout, onChange } ) { ) : ( diff --git a/packages/block-library/src/post-excerpt/edit.js b/packages/block-library/src/post-excerpt/edit.js index 45d0a79a411d61..05aaf543b59196 100644 --- a/packages/block-library/src/post-excerpt/edit.js +++ b/packages/block-library/src/post-excerpt/edit.js @@ -231,6 +231,7 @@ export default function PostExcerptEditor( { } /> Date: Sat, 17 Aug 2024 05:51:32 +0800 Subject: [PATCH 126/126] CustomSelectControl: improve props type inferring (#64412) * CustomSelectControl: improve props type inferring * chore: update changelog * chore: updates Co-authored-by: meteorlxy Co-authored-by: mirka <0mirka00@git.wordpress.org> --- packages/components/CHANGELOG.md | 4 + .../src/custom-select-control/test/index.tsx | 125 ++++++++++++++++++ .../src/custom-select-control/types.ts | 6 +- 3 files changed, 132 insertions(+), 3 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index f824bf8d2b2ddc..f1b6ba3341de60 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -67,6 +67,10 @@ - `Composite` v2: add focus-related props to `Composite`and`Composite.Item` subcomponents ([#64450](https://github.com/WordPress/gutenberg/pull/64450)). - `Composite` v2: add `Context` subcomponent ([#64493](https://github.com/WordPress/gutenberg/pull/64493)). +### Internal + +- `CustomSelectControl`: Improve type inferring ([#64412](https://github.com/WordPress/gutenberg/pull/64412)). + ## 28.5.0 (2024-08-07) ### Bug Fixes diff --git a/packages/components/src/custom-select-control/test/index.tsx b/packages/components/src/custom-select-control/test/index.tsx index fdbe8d72a48dec..b2ac5c19c6ab3f 100644 --- a/packages/components/src/custom-select-control/test/index.tsx +++ b/packages/components/src/custom-select-control/test/index.tsx @@ -689,3 +689,128 @@ describe.each( [ } ); } ); } ); + +describe( 'Type checking', () => { + // eslint-disable-next-line jest/expect-expect + it( 'should infer the value type from available `options`, but not the `value` or `onChange` prop', () => { + const options = [ + { + key: 'narrow', + name: 'Narrow', + }, + { + key: 'value', + name: 'Value', + }, + ]; + const optionsReadOnly = [ + { + key: 'narrow', + name: 'Narrow', + }, + { + key: 'value', + name: 'Value', + }, + ] as const; + + const onChange = (): void => {}; + + ; + + ; + + ; + + void + } + />; + + ; + + ; + + ; + + void + } + />; + } ); +} ); diff --git a/packages/components/src/custom-select-control/types.ts b/packages/components/src/custom-select-control/types.ts index e37ba349a2b843..0cbc2388e79638 100644 --- a/packages/components/src/custom-select-control/types.ts +++ b/packages/components/src/custom-select-control/types.ts @@ -55,7 +55,7 @@ export type CustomSelectProps< T extends CustomSelectOption > = { * Function called with the control's internal state changes. The `selectedItem` * property contains the next selected item. */ - onChange?: ( newValue: CustomSelectChangeObject< T > ) => void; + onChange?: ( newValue: CustomSelectChangeObject< NoInfer< T > > ) => void; /** * A handler for `blur` events on the trigger button. * @@ -83,7 +83,7 @@ export type CustomSelectProps< T extends CustomSelectOption > = { /** * The list of options that can be chosen from. */ - options: Array< T >; + options: ReadonlyArray< T >; /** * The size of the control. * @@ -93,7 +93,7 @@ export type CustomSelectProps< T extends CustomSelectOption > = { /** * Can be used to externally control the value of the control. */ - value?: T; + value?: NoInfer< T >; /** * Use the `showSelectedHint` property instead. * @deprecated