From 33e2fadeebe32a64a946106488120b6f559d3deb Mon Sep 17 00:00:00 2001 From: Mikey Binns <38146638+mikeybinns@users.noreply.github.com> Date: Fri, 13 Sep 2024 11:34:14 +0100 Subject: [PATCH 01/17] Styleling config: Fix stylelint config missing files for npm (#65313) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add stylistic configs to packaged files --------- Co-authored-by: Greg Ziółkowski Co-authored-by: mikeybinns Co-authored-by: gziolo --- packages/stylelint-config/package.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/stylelint-config/package.json b/packages/stylelint-config/package.json index 53051078b9b86..874ec2af0c664 100644 --- a/packages/stylelint-config/package.json +++ b/packages/stylelint-config/package.json @@ -28,7 +28,9 @@ "LICENSE", "README.md", "index.js", - "scss.js" + "scss.js", + "scss-stylistic.js", + "stylistic.js" ], "main": "index.js", "dependencies": { From dfc28b08c077818cda4b7ac29cc7efebfa865aba Mon Sep 17 00:00:00 2001 From: Carlos Bravo <37012961+cbravobernal@users.noreply.github.com> Date: Fri, 13 Sep 2024 13:59:10 +0200 Subject: [PATCH 02/17] Patterns: Change deprecated social icons for standard in e2e (#65312) * Change deprecated social icons for standard * Update more tests * Update test/e2e/specs/editor/various/adding-patterns.spec.js Co-authored-by: Kai Hao * One more spec * Update snapshots * Revert changes to post-editor.spec.js --------- Co-authored-by: cbravobernal Co-authored-by: t-hamano Co-authored-by: kevin940726 Co-authored-by: Mamaduka --- ...ing-from-the-global-inserter-1-chromium.txt | 18 +++++++++++++----- ...pping-from-the-global-inserter-1-webkit.txt | 18 +++++++++++++----- .../editor/various/adding-patterns.spec.js | 8 +++----- .../editor/various/inserting-blocks.spec.js | 4 ++-- 4 files changed, 31 insertions(+), 17 deletions(-) diff --git a/test/e2e/specs/editor/various/__snapshots__/Inserting-blocks-firefox-webkit-inserts-p-59603-ragging-and-dropping-from-the-global-inserter-1-chromium.txt b/test/e2e/specs/editor/various/__snapshots__/Inserting-blocks-firefox-webkit-inserts-p-59603-ragging-and-dropping-from-the-global-inserter-1-chromium.txt index 45d99c7c27e6f..753c30a092670 100644 --- a/test/e2e/specs/editor/various/__snapshots__/Inserting-blocks-firefox-webkit-inserts-p-59603-ragging-and-dropping-from-the-global-inserter-1-chromium.txt +++ b/test/e2e/specs/editor/various/__snapshots__/Inserting-blocks-firefox-webkit-inserts-p-59603-ragging-and-dropping-from-the-global-inserter-1-chromium.txt @@ -2,10 +2,18 @@

Dummy text

- - - \ No newline at end of file + + + +
+ + + + + \ No newline at end of file diff --git a/test/e2e/specs/editor/various/__snapshots__/Inserting-blocks-firefox-webkit-inserts-p-59603-ragging-and-dropping-from-the-global-inserter-1-webkit.txt b/test/e2e/specs/editor/various/__snapshots__/Inserting-blocks-firefox-webkit-inserts-p-59603-ragging-and-dropping-from-the-global-inserter-1-webkit.txt index 45d99c7c27e6f..753c30a092670 100644 --- a/test/e2e/specs/editor/various/__snapshots__/Inserting-blocks-firefox-webkit-inserts-p-59603-ragging-and-dropping-from-the-global-inserter-1-webkit.txt +++ b/test/e2e/specs/editor/various/__snapshots__/Inserting-blocks-firefox-webkit-inserts-p-59603-ragging-and-dropping-from-the-global-inserter-1-webkit.txt @@ -2,10 +2,18 @@

Dummy text

- - - \ No newline at end of file + + + +
+ + + + + \ No newline at end of file diff --git a/test/e2e/specs/editor/various/adding-patterns.spec.js b/test/e2e/specs/editor/various/adding-patterns.spec.js index 2846c7adac271..35192dd364dc6 100644 --- a/test/e2e/specs/editor/various/adding-patterns.spec.js +++ b/test/e2e/specs/editor/various/adding-patterns.spec.js @@ -14,15 +14,13 @@ test.describe( 'adding patterns', () => { await page.getByRole( 'tab', { name: 'Patterns' } ).click(); await page.fill( 'role=region[name="Block Library"i] >> role=searchbox[name="Search for blocks and patterns"i]', - 'Social links with a shared background color' + 'Standard' ); - await page.click( - 'role=option[name="Social links with a shared background color"i]' - ); + await page.getByRole( 'option', { name: 'Standard' } ).click(); await expect.poll( editor.getBlocks ).toMatchObject( [ { - name: 'core/social-links', + name: 'core/query', }, ] ); } ); diff --git a/test/e2e/specs/editor/various/inserting-blocks.spec.js b/test/e2e/specs/editor/various/inserting-blocks.spec.js index 1a443152800de..b4f23c8b8e2bb 100644 --- a/test/e2e/specs/editor/various/inserting-blocks.spec.js +++ b/test/e2e/specs/editor/various/inserting-blocks.spec.js @@ -194,7 +194,7 @@ test.describe( 'Inserting blocks (@firefox, @webkit)', () => { 'role=region[name="Editor top bar"i] >> role=button[name="Toggle block inserter"i]' ); - const PATTERN_NAME = 'Social links with a shared background color'; + const PATTERN_NAME = 'Standard'; await page.fill( 'role=region[name="Block Library"i] >> role=searchbox[name="Search for blocks and patterns"i]', @@ -350,7 +350,7 @@ test.describe( 'Inserting blocks (@firefox, @webkit)', () => { 'role=region[name="Editor top bar"i] >> role=button[name="Toggle block inserter"i]' ); - const PATTERN_NAME = 'Social links with a shared background color'; + const PATTERN_NAME = 'Standard'; await page.fill( 'role=region[name="Block Library"i] >> role=searchbox[name="Search for blocks and patterns"i]', From 12660ca05ef04dd0e774c943e78ac4147af2b5ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9?= <583546+oandregal@users.noreply.github.com> Date: Fri, 13 Sep 2024 15:13:56 +0200 Subject: [PATCH 03/17] DataViews: improve UX of bundled views for Pages (#65295) Co-authored-by: oandregal Co-authored-by: youknowriad Co-authored-by: jameskoster Co-authored-by: Souptik2001 Co-authored-by: ntsekouras Co-authored-by: SaxonF Co-authored-by: jasmussen --- .../src/components/post-list/index.js | 50 ++++++++- .../sidebar-dataviews/default-views.js | 104 ++++++++---------- 2 files changed, 95 insertions(+), 59 deletions(-) diff --git a/packages/edit-site/src/components/post-list/index.js b/packages/edit-site/src/components/post-list/index.js index bbfece2451849..6738310ff6021 100644 --- a/packages/edit-site/src/components/post-list/index.js +++ b/packages/edit-site/src/components/post-list/index.js @@ -179,6 +179,7 @@ function getItemId( item ) { export default function PostList( { postType } ) { const [ view, setView ] = useView( postType ); + const defaultViews = useDefaultViews( { postType } ); const history = useHistory(); const location = useLocation(); const { @@ -202,7 +203,26 @@ export default function PostList( { postType } ) { [ history ] ); - const { isLoading: isLoadingFields, fields } = usePostFields( view.type ); + const getActiveViewFilters = ( views, match ) => { + const found = views.find( ( { slug } ) => slug === match ); + return found?.filters ?? []; + }; + + const { isLoading: isLoadingFields, fields: _fields } = usePostFields( + view.type + ); + const fields = useMemo( () => { + const activeViewFilters = getActiveViewFilters( + defaultViews, + activeView + ).map( ( { field } ) => field ); + return _fields.map( ( field ) => ( { + ...field, + elements: activeViewFilters.includes( field.id ) + ? [] + : field.elements, + } ) ); + }, [ _fields, defaultViews, activeView ] ); const queryArgs = useMemo( () => { const filters = {}; @@ -225,6 +245,32 @@ export default function PostList( { postType } ) { filters.author_exclude = filter.value; } } ); + + // The bundled views want data filtered without displaying the filter. + const activeViewFilters = getActiveViewFilters( + defaultViews, + activeView + ); + activeViewFilters.forEach( ( filter ) => { + if ( + filter.field === 'status' && + filter.operator === OPERATOR_IS_ANY + ) { + filters.status = filter.value; + } + if ( + filter.field === 'author' && + filter.operator === OPERATOR_IS_ANY + ) { + filters.author = filter.value; + } else if ( + filter.field === 'author' && + filter.operator === OPERATOR_IS_NONE + ) { + filters.author_exclude = filter.value; + } + } ); + // We want to provide a different default item for the status filter // than the REST API provides. if ( ! filters.status || filters.status === '' ) { @@ -240,7 +286,7 @@ export default function PostList( { postType } ) { search: view.search, ...filters, }; - }, [ view ] ); + }, [ view, activeView, defaultViews ] ); const { records, isResolving: isLoadingData, diff --git a/packages/edit-site/src/components/sidebar-dataviews/default-views.js b/packages/edit-site/src/components/sidebar-dataviews/default-views.js index e5db492fce17d..658fa319e9c66 100644 --- a/packages/edit-site/src/components/sidebar-dataviews/default-views.js +++ b/packages/edit-site/src/components/sidebar-dataviews/default-views.js @@ -88,76 +88,66 @@ export function useDefaultViews( { postType } ) { title: __( 'Published' ), slug: 'published', icon: published, - view: { - ...DEFAULT_POST_BASE, - filters: [ - { - field: 'status', - operator: OPERATOR_IS_ANY, - value: 'publish', - }, - ], - }, + view: DEFAULT_POST_BASE, + filters: [ + { + field: 'status', + operator: OPERATOR_IS_ANY, + value: 'publish', + }, + ], }, { title: __( 'Scheduled' ), slug: 'future', icon: scheduled, - view: { - ...DEFAULT_POST_BASE, - filters: [ - { - field: 'status', - operator: OPERATOR_IS_ANY, - value: 'future', - }, - ], - }, + view: DEFAULT_POST_BASE, + filters: [ + { + field: 'status', + operator: OPERATOR_IS_ANY, + value: 'future', + }, + ], }, { title: __( 'Drafts' ), slug: 'drafts', icon: drafts, - view: { - ...DEFAULT_POST_BASE, - filters: [ - { - field: 'status', - operator: OPERATOR_IS_ANY, - value: 'draft', - }, - ], - }, + view: DEFAULT_POST_BASE, + filters: [ + { + field: 'status', + operator: OPERATOR_IS_ANY, + value: 'draft', + }, + ], }, { title: __( 'Pending' ), slug: 'pending', icon: pending, - view: { - ...DEFAULT_POST_BASE, - filters: [ - { - field: 'status', - operator: OPERATOR_IS_ANY, - value: 'pending', - }, - ], - }, + view: DEFAULT_POST_BASE, + filters: [ + { + field: 'status', + operator: OPERATOR_IS_ANY, + value: 'pending', + }, + ], }, { title: __( 'Private' ), slug: 'private', icon: notAllowed, - view: { - ...DEFAULT_POST_BASE, - filters: [ - { - field: 'status', - operator: OPERATOR_IS_ANY, - value: 'private', - }, - ], - }, + view: DEFAULT_POST_BASE, + filters: [ + { + field: 'status', + operator: OPERATOR_IS_ANY, + value: 'private', + }, + ], }, { title: __( 'Trash' ), @@ -167,14 +157,14 @@ export function useDefaultViews( { postType } ) { ...DEFAULT_POST_BASE, type: LAYOUT_TABLE, layout: defaultLayouts[ LAYOUT_TABLE ].layout, - filters: [ - { - field: 'status', - operator: OPERATOR_IS_ANY, - value: 'trash', - }, - ], }, + filters: [ + { + field: 'status', + operator: OPERATOR_IS_ANY, + value: 'trash', + }, + ], }, ]; }, [ labels ] ); From b0abc74391809c33675471070660ed2519f22c9b Mon Sep 17 00:00:00 2001 From: Mitchell Austin Date: Fri, 13 Sep 2024 06:37:53 -0700 Subject: [PATCH 04/17] Split content view with meta boxes (#64351) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Use iframe even with metaboxes * Split content view when metaboxes are open * Fix styling issues Co-authored-by: t-hamano * Update e2e tests Co-authored-by: ellatrix * Make metaboxes a details element in short viewports * Make metabox area resizable when viewport isn’t short * Tweak details element usage * Add max-height when resized by user * Hide metabox area if all metaboxes are hidden * Persist toggle and height state in user preferences * Wrap contents and rename things The added wrapping element was due to Safari clipping the drop-shadow when `overflow: auto` was on the component root. * Make visual editor stacking context when iframed with metaboxes * Omit meta box area when empty instead of hiding it * Fix meta boxes content appearing in front of UI * Use split views only when canvas is iframed * Don’t omit main meta box area unless all locations aren’t visible --------- Co-authored-by: t-hamano Co-authored-by: ellatrix Co-authored-by: youknowriad Co-authored-by: ciampo Co-authored-by: torounit Co-authored-by: talldan Co-authored-by: arnaudbroes --- .../edit-post/src/components/layout/index.js | 128 +++++++++++++++++- .../src/components/layout/style.scss | 70 +++++++++- .../components/layout/use-should-iframe.js | 11 +- packages/edit-post/src/store/selectors.js | 1 - .../specs/editor/plugins/meta-boxes.spec.js | 18 ++- .../editor/plugins/wp-editor-meta-box.spec.js | 2 +- .../editor/various/publish-button.spec.js | 7 +- 7 files changed, 205 insertions(+), 32 deletions(-) diff --git a/packages/edit-post/src/components/layout/index.js b/packages/edit-post/src/components/layout/index.js index 185f97ba45a56..2cbf5c32e814b 100644 --- a/packages/edit-post/src/components/layout/index.js +++ b/packages/edit-post/src/components/layout/index.js @@ -24,7 +24,12 @@ import { } from '@wordpress/block-editor'; import { PluginArea } from '@wordpress/plugins'; import { __, sprintf } from '@wordpress/i18n'; -import { useCallback, useMemo } from '@wordpress/element'; +import { + useCallback, + useLayoutEffect, + useMemo, + useRef, +} from '@wordpress/element'; import { store as noticesStore } from '@wordpress/notices'; import { store as preferencesStore } from '@wordpress/preferences'; import { @@ -36,8 +41,8 @@ import { privateApis as blockLibraryPrivateApis } from '@wordpress/block-library import { addQueryArgs } from '@wordpress/url'; import { decodeEntities } from '@wordpress/html-entities'; import { store as coreStore } from '@wordpress/core-data'; -import { SlotFillProvider } from '@wordpress/components'; -import { useViewportMatch } from '@wordpress/compose'; +import { ResizableBox, SlotFillProvider } from '@wordpress/components'; +import { useMediaQuery, useViewportMatch } from '@wordpress/compose'; /** * Internal dependencies @@ -151,6 +156,118 @@ function useEditorStyles() { ] ); } +/** + * @param {Object} props + * @param {boolean} props.isLegacy True when the editor canvas is not in an iframe. + */ +function MetaBoxesMain( { isLegacy } ) { + const [ isOpen, openHeight, hasAnyVisible ] = useSelect( ( select ) => { + const { get } = select( preferencesStore ); + const { isMetaBoxLocationVisible } = select( editPostStore ); + return [ + get( 'core/edit-post', 'metaBoxesMainIsOpen' ), + get( 'core/edit-post', 'metaBoxesMainOpenHeight' ), + isMetaBoxLocationVisible( 'normal' ) || + isMetaBoxLocationVisible( 'advanced' ) || + isMetaBoxLocationVisible( 'side' ), + ]; + }, [] ); + const { set: setPreference } = useDispatch( preferencesStore ); + const resizableBoxRef = useRef(); + const isShort = useMediaQuery( '(max-height: 549px)' ); + + const isAutoHeight = openHeight === undefined; + // In case a user size is set stops the default max-height from applying. + useLayoutEffect( () => { + if ( ! isLegacy && hasAnyVisible && ! isShort && ! isAutoHeight ) { + resizableBoxRef.current.resizable.classList.add( 'has-user-size' ); + } + }, [ isAutoHeight, isShort, hasAnyVisible, isLegacy ] ); + + if ( ! hasAnyVisible ) { + return; + } + + const className = 'edit-post-meta-boxes-main'; + const contents = ( +
+ + +
+ ); + + if ( isLegacy ) { + return contents; + } + + if ( isShort ) { + return ( +
{ + setPreference( + 'core/edit-post', + 'metaBoxesMainIsOpen', + target.open + ); + } } + > + { __( 'Meta Boxes' ) } + { contents } +
+ ); + } + return ( + { + target.setPointerCapture( pointerId ); + } } + onResizeStart={ ( event, direction, elementRef ) => { + // Avoids height jumping in case it’s limited by max-height. + elementRef.style.height = `${ elementRef.offsetHeight }px`; + // Stops initial max-height from being applied. + elementRef.classList.add( 'has-user-size' ); + } } + onResizeStop={ () => { + setPreference( + 'core/edit-post', + 'metaBoxesMainOpenHeight', + resizableBoxRef.current.state.height + ); + } } + > + { contents } + + ); +} + function Layout( { postId: initialPostId, postType: initialPostType, @@ -355,10 +472,7 @@ function Layout( { extraContent={ ! isDistractionFree && showMetaBoxes && ( -
- - -
+ ) } > diff --git a/packages/edit-post/src/components/layout/style.scss b/packages/edit-post/src/components/layout/style.scss index 5fdaceaa002be..5392a7e9da4d9 100644 --- a/packages/edit-post/src/components/layout/style.scss +++ b/packages/edit-post/src/components/layout/style.scss @@ -1,6 +1,70 @@ -.edit-post-layout__metaboxes { - flex-shrink: 0; - clear: both; +.edit-post-meta-boxes-main { + filter: drop-shadow(0 -1px rgba($color: #000, $alpha: 0.133)); // 0.133 = $gray-200 but with alpha. + background-color: $white; + clear: both; // This is seemingly only needed in case the canvas is not iframe’d. + + &:not(details) { + padding-top: 23px; + max-height: 100%; + + &:not(.has-user-size) { + max-height: 50% !important; + } + } + + // The component renders as a details element in short viewports. + &:is(details) { + & > summary { + cursor: pointer; + color: $gray-900; + background-color: $white; + height: $button-size-compact; + line-height: $button-size-compact; + font-size: 13px; + padding-left: $grid-unit-30; + box-shadow: 0 $border-width $gray-300; + } + + &[open] > summary { + position: sticky; + top: 0; + z-index: 1; + } + } + + & .components-resizable-box__handle-top { + top: 0; + box-shadow: 0 $border-width $gray-300; + } + & .components-resizable-box__side-handle::before { + border-radius: 0; + top: 0; + height: $border-width; + } + & .components-resizable-box__handle::after { + background-color: $gray-300; + box-shadow: none; + border-radius: 4px; + height: $grid-unit-05; + top: calc(50% - #{$grid-unit-05} / 2); + width: 100px; + right: calc(50% - 50px); + } +} + +.edit-post-meta-boxes-main__liner { + overflow: auto; + max-height: 100%; + // Keep the contents behind the resize handle or details summary. + isolation: isolate; +} + +.has-metaboxes .editor-visual-editor { + flex: 1; + + &.is-iframed { + isolation: isolate; + } } // Adjust the position of the notices 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 248ea53109f25..e36a4773c4a1f 100644 --- a/packages/edit-post/src/components/layout/use-should-iframe.js +++ b/packages/edit-post/src/components/layout/use-should-iframe.js @@ -6,11 +6,6 @@ import { useSelect } from '@wordpress/data'; import { store as blocksStore } from '@wordpress/blocks'; import { store as blockEditorStore } from '@wordpress/block-editor'; -/** - * Internal dependencies - */ -import { store as editPostStore } from '../../store'; - const isGutenbergPlugin = globalThis.IS_GUTENBERG_PLUGIN ? true : false; export function useShouldIframe() { @@ -18,7 +13,6 @@ export function useShouldIframe() { isBlockBasedTheme, hasV3BlocksOnly, isEditingTemplate, - hasMetaBoxes, isZoomOutMode, } = useSelect( ( select ) => { const { getEditorSettings, getCurrentPostType } = select( editorStore ); @@ -31,14 +25,13 @@ export function useShouldIframe() { return type.apiVersion >= 3; } ), isEditingTemplate: getCurrentPostType() === 'wp_template', - hasMetaBoxes: select( editPostStore ).hasMetaBoxes(), isZoomOutMode: __unstableGetEditorMode() === 'zoom-out', }; }, [] ); return ( - ( ( hasV3BlocksOnly || ( isGutenbergPlugin && isBlockBasedTheme ) ) && - ! hasMetaBoxes ) || + hasV3BlocksOnly || + ( isGutenbergPlugin && isBlockBasedTheme ) || isEditingTemplate || isZoomOutMode ); diff --git a/packages/edit-post/src/store/selectors.js b/packages/edit-post/src/store/selectors.js index da1f9959e32e9..5bea6e7d35eb6 100644 --- a/packages/edit-post/src/store/selectors.js +++ b/packages/edit-post/src/store/selectors.js @@ -379,7 +379,6 @@ export const isMetaBoxLocationVisible = createRegistrySelector( isMetaBoxLocationActive( state, location ) && getMetaBoxesPerLocation( state, location )?.some( ( { id } ) => { return select( editorStore ).isEditorPanelEnabled( - state, `meta-box-${ id }` ); } ) diff --git a/test/e2e/specs/editor/plugins/meta-boxes.spec.js b/test/e2e/specs/editor/plugins/meta-boxes.spec.js index 1b7adc18760ff..511b3837f8030 100644 --- a/test/e2e/specs/editor/plugins/meta-boxes.spec.js +++ b/test/e2e/specs/editor/plugins/meta-boxes.spec.js @@ -26,7 +26,7 @@ test.describe( 'Meta boxes', () => { await expect( saveDraft ).toBeDisabled(); // Add title to enable valid non-empty post save. - await page + await editor.canvas .getByRole( 'textbox', { name: 'Add title' } ) .fill( 'Hello Meta' ); @@ -44,7 +44,7 @@ test.describe( 'Meta boxes', () => { page, } ) => { // Publish a post so there's something for the latest posts dynamic block to render. - await page + await editor.canvas .getByRole( 'textbox', { name: 'Add title' } ) .fill( 'A published post' ); await page.keyboard.press( 'Enter' ); @@ -53,7 +53,7 @@ test.describe( 'Meta boxes', () => { // Publish a post with the latest posts dynamic block. await admin.createNewPost(); - await page + await editor.canvas .getByRole( 'textbox', { name: 'Add title' } ) .fill( 'Dynamic block test' ); await editor.insertBlock( { name: 'core/latest-posts' } ); @@ -70,10 +70,12 @@ test.describe( 'Meta boxes', () => { editor, page, } ) => { - await page + await editor.canvas .getByRole( 'textbox', { name: 'Add title' } ) .fill( 'A published post' ); - await page.getByRole( 'button', { name: 'Add default block' } ).click(); + await editor.canvas + .getByRole( 'button', { name: 'Add default block' } ) + .click(); await page.keyboard.type( 'Excerpt from content.' ); const postId = await editor.publishPost(); @@ -89,9 +91,11 @@ test.describe( 'Meta boxes', () => { page, } ) => { await editor.openDocumentSettingsSidebar(); - await page.getByRole( 'button', { name: 'Add default block' } ).click(); + await editor.canvas + .getByRole( 'button', { name: 'Add default block' } ) + .click(); await page.keyboard.type( 'Excerpt from content.' ); - await page + await editor.canvas .getByRole( 'textbox', { name: 'Add title' } ) .fill( 'A published post' ); diff --git a/test/e2e/specs/editor/plugins/wp-editor-meta-box.spec.js b/test/e2e/specs/editor/plugins/wp-editor-meta-box.spec.js index 710e06b35e124..c5eafdafe918d 100644 --- a/test/e2e/specs/editor/plugins/wp-editor-meta-box.spec.js +++ b/test/e2e/specs/editor/plugins/wp-editor-meta-box.spec.js @@ -20,7 +20,7 @@ test.describe( 'WP Editor Meta Boxes', () => { await admin.createNewPost(); // Add title to enable valid non-empty post save. - await page + await editor.canvas .locator( 'role=textbox[name="Add title"i]' ) .type( 'Hello Meta' ); diff --git a/test/e2e/specs/editor/various/publish-button.spec.js b/test/e2e/specs/editor/various/publish-button.spec.js index 631ddcd0fe61b..cdc9c1a993636 100644 --- a/test/e2e/specs/editor/various/publish-button.spec.js +++ b/test/e2e/specs/editor/various/publish-button.spec.js @@ -70,13 +70,12 @@ test.describe( 'Post publish button', () => { admin, page, requestUtils, + editor, } ) => { await requestUtils.activatePlugin( 'gutenberg-test-plugin-meta-box' ); await admin.createNewPost(); - await page - .getByRole( 'textbox', { - name: 'Add title', - } ) + await editor.canvas + .getByRole( 'textbox', { name: 'Add title' } ) .fill( 'Test post' ); const topBar = page.getByRole( 'region', { name: 'Editor top bar' } ); From 9e88d8666d097eeaa307009c726ab35cfc940d5a Mon Sep 17 00:00:00 2001 From: Peter Wilson <519727+peterwilsoncc@users.noreply.github.com> Date: Sat, 14 Sep 2024 00:30:30 +1000 Subject: [PATCH 05/17] Update minimum required version in PHP. (#65301) * Update minimum required version in PHP. * Use a constant. * Test plugin meta data and constant match. * Set constant incorrectly to demonstrate failing tests. * Use regex to extract the constant. * Match the version in the constant and plugin meta. Co-authored-by: peterwilsoncc Co-authored-by: Mamaduka --- gutenberg.php | 6 +++-- .../class-test-plugin-version-meta-test.php | 26 +++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 phpunit/class-test-plugin-version-meta-test.php diff --git a/gutenberg.php b/gutenberg.php index c9c6e09a8092b..342c61fb56cd1 100644 --- a/gutenberg.php +++ b/gutenberg.php @@ -15,6 +15,8 @@ ### BEGIN AUTO-GENERATED DEFINES defined( 'GUTENBERG_DEVELOPMENT_MODE' ) or define( 'GUTENBERG_DEVELOPMENT_MODE', true ); ### END AUTO-GENERATED DEFINES +defined( 'GUTENBERG_MINIMUM_WP_VERSION' ) or define( 'GUTENBERG_MINIMUM_WP_VERSION', '6.5' ); + gutenberg_pre_init(); @@ -26,7 +28,7 @@ function gutenberg_wordpress_version_notice() { echo '

'; /* translators: %s: Minimum required version */ - printf( __( 'Gutenberg requires WordPress %s or later to function properly. Please upgrade WordPress before activating Gutenberg.', 'gutenberg' ), '5.9' ); + printf( __( 'Gutenberg requires WordPress %s or later to function properly. Please upgrade WordPress before activating Gutenberg.', 'gutenberg' ), GUTENBERG_MINIMUM_WP_VERSION ); echo '

'; deactivate_plugins( array( 'gutenberg/gutenberg.php' ) ); @@ -67,7 +69,7 @@ function gutenberg_pre_init() { // Compare against major release versions (X.Y) rather than minor (X.Y.Z) // unless a minor release is the actual minimum requirement. WordPress reports // X.Y for its major releases. - if ( version_compare( $version, '5.9', '<' ) ) { + if ( version_compare( $version, GUTENBERG_MINIMUM_WP_VERSION, '<' ) ) { add_action( 'admin_notices', 'gutenberg_wordpress_version_notice' ); return; } diff --git a/phpunit/class-test-plugin-version-meta-test.php b/phpunit/class-test-plugin-version-meta-test.php new file mode 100644 index 0000000000000..f0dadf873166f --- /dev/null +++ b/phpunit/class-test-plugin-version-meta-test.php @@ -0,0 +1,26 @@ + 'Requires at least' ) ); + /* + * Gutenberg.php isn't loaded in the test environment. + * + * Read the file directly and use regex to extract the constant. + */ + preg_match( '/GUTENBERG_MINIMUM_WP_VERSION\', \'(.*?)\'/', file_get_contents( __DIR__ . '/../gutenberg.php' ), $matches ); + $version_in_file = $matches[1]; + + $this->assertSame( $file_meta['RequiresWP'], $version_in_file, 'The minimum required WordPress version does not match the plugin header.' ); + } +} From 257d26aecc8abaac6edad3876aede23603be3efe Mon Sep 17 00:00:00 2001 From: Aki Hamano <54422211+t-hamano@users.noreply.github.com> Date: Fri, 13 Sep 2024 23:53:34 +0900 Subject: [PATCH 06/17] DimensionsPanel: Apply 40px default size to UI when no spacing preset is available (#65300) * DimensionsPanel: Apply 40px default size to UI when no spacing preset is available * Revert unnecessary change Co-authored-by: t-hamano Co-authored-by: jasmussen --- .../src/components/global-styles/dimensions-panel.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/block-editor/src/components/global-styles/dimensions-panel.js b/packages/block-editor/src/components/global-styles/dimensions-panel.js index 8430703aec966..ce508a5ebc89e 100644 --- a/packages/block-editor/src/components/global-styles/dimensions-panel.js +++ b/packages/block-editor/src/components/global-styles/dimensions-panel.js @@ -531,6 +531,7 @@ export default function DimensionsPanel( { > { ! showSpacingPresetsControl && ( { ! showSpacingPresetsControl && ( Date: Fri, 13 Sep 2024 20:05:11 +0300 Subject: [PATCH 07/17] DataViews: Use Dropdown for views config dialog (#65314) Co-authored-by: ntsekouras Co-authored-by: jameskoster --- .../dataviews-view-config/index.tsx | 48 +++++++++---------- .../dataviews-view-config/style.scss | 15 +++--- 2 files changed, 32 insertions(+), 31 deletions(-) diff --git a/packages/dataviews/src/components/dataviews-view-config/index.tsx b/packages/dataviews/src/components/dataviews-view-config/index.tsx index c01c72d2ebc69..48fdf6906b077 100644 --- a/packages/dataviews/src/components/dataviews-view-config/index.tsx +++ b/packages/dataviews/src/components/dataviews-view-config/index.tsx @@ -8,7 +8,7 @@ import type { ChangeEvent } from 'react'; */ import { Button, - Popover, + Dropdown, __experimentalToggleGroupControl as ToggleGroupControl, __experimentalToggleGroupControlOption as ToggleGroupControlOption, __experimentalToggleGroupControlOptionIcon as ToggleGroupControlOptionIcon, @@ -24,7 +24,7 @@ import { BaseControl, } from '@wordpress/components'; import { __, _x, sprintf } from '@wordpress/i18n'; -import { memo, useContext, useState, useMemo } from '@wordpress/element'; +import { memo, useContext, useMemo } from '@wordpress/element'; import { chevronDown, chevronUp, cog, seen, unseen } from '@wordpress/icons'; import warning from '@wordpress/warning'; @@ -549,34 +549,32 @@ function _DataViewsViewConfig( { setDensity: React.Dispatch< React.SetStateAction< number > >; defaultLayouts?: SupportedLayouts; } ) { - const [ isShowingViewPopover, setIsShowingViewPopover ] = - useState< boolean >( false ); - return ( <> -
-
+ /> ); } diff --git a/packages/dataviews/src/components/dataviews-view-config/style.scss b/packages/dataviews/src/components/dataviews-view-config/style.scss index 4c01de6b8994e..c7d07fe7866bc 100644 --- a/packages/dataviews/src/components/dataviews-view-config/style.scss +++ b/packages/dataviews/src/components/dataviews-view-config/style.scss @@ -1,11 +1,14 @@ .dataviews-view-config { - width: 320px; - /* stylelint-disable-next-line property-no-unknown -- the linter needs to be updated to accepted the container-type property */ - container-type: inline-size; - padding: $grid-unit-20; - font-size: $default-font-size; - line-height: $default-line-height; + .components-popover__content { + width: 320px; + /* stylelint-disable-next-line property-no-unknown -- the linter needs to be updated to accepted the container-type property */ + container-type: inline-size; + padding: $grid-unit-20; + font-size: $default-font-size; + line-height: $default-line-height; + } } + .dataviews-view-config__sort-direction .components-toggle-group-control-option-base { text-transform: uppercase; } From e1020339adca55fa73d33020dbf28cf779e387da Mon Sep 17 00:00:00 2001 From: George Mamadashvili Date: Sat, 14 Sep 2024 00:40:23 +0400 Subject: [PATCH 08/17] Block Editor: Use static access for selector in 'useZoomOutModeExit' (#65337) Co-authored-by: Mamaduka --- .../block-list/use-block-props/use-zoom-out-mode-exit.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/block-editor/src/components/block-list/use-block-props/use-zoom-out-mode-exit.js b/packages/block-editor/src/components/block-list/use-block-props/use-zoom-out-mode-exit.js index 1944cfde96c7f..d0001bd3b33c6 100644 --- a/packages/block-editor/src/components/block-list/use-block-props/use-zoom-out-mode-exit.js +++ b/packages/block-editor/src/components/block-list/use-block-props/use-zoom-out-mode-exit.js @@ -16,10 +16,7 @@ import { unlock } from '../../../lock-unlock'; * @param {string} clientId Block client ID. */ export function useZoomOutModeExit( { editorMode } ) { - const getSettings = useSelect( - ( select ) => select( blockEditorStore ).getSettings - ); - + const { getSettings } = useSelect( blockEditorStore ); const { __unstableSetEditorMode } = unlock( useDispatch( blockEditorStore ) ); @@ -51,6 +48,6 @@ export function useZoomOutModeExit( { editorMode } ) { node.removeEventListener( 'dblclick', onDoubleClick ); }; }, - [ editorMode, __unstableSetEditorMode ] + [ editorMode, getSettings, __unstableSetEditorMode ] ); } From 1df91fb8289db22837ab68030ef7d87c90a2ed72 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Fri, 13 Sep 2024 23:13:32 +0200 Subject: [PATCH 09/17] Add: Typography styling support to the navigation submenu block. (#65060) co-authored-by: jorgefilipecosta Co-authored-by: Mamaduka Co-authored-by: annezazu Co-authored-by: cuemarie --- docs/reference-guides/core-blocks.md | 2 +- .../block-library/src/navigation-submenu/block.json | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/docs/reference-guides/core-blocks.md b/docs/reference-guides/core-blocks.md index b9cae44550181..0e0de9d4f8a50 100644 --- a/docs/reference-guides/core-blocks.md +++ b/docs/reference-guides/core-blocks.md @@ -493,7 +493,7 @@ Add a submenu to your navigation. ([Source](https://github.com/WordPress/gutenbe - **Name:** core/navigation-submenu - **Category:** design - **Parent:** core/navigation -- **Supports:** interactivity (clientNavigation), ~~html~~, ~~reusable~~ +- **Supports:** interactivity (clientNavigation), typography (fontSize, lineHeight), ~~html~~, ~~reusable~~ - **Attributes:** description, id, isTopLevelItem, kind, label, opensInNewTab, rel, title, type, url ## Page Break diff --git a/packages/block-library/src/navigation-submenu/block.json b/packages/block-library/src/navigation-submenu/block.json index 0bcf8a1a47f38..e767b60aec3a6 100644 --- a/packages/block-library/src/navigation-submenu/block.json +++ b/packages/block-library/src/navigation-submenu/block.json @@ -59,6 +59,19 @@ "supports": { "reusable": false, "html": false, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalFontFamily": true, + "__experimentalFontWeight": true, + "__experimentalFontStyle": true, + "__experimentalTextTransform": true, + "__experimentalTextDecoration": true, + "__experimentalLetterSpacing": true, + "__experimentalDefaultControls": { + "fontSize": true + } + }, "interactivity": { "clientNavigation": true } From b10ca396e14fc0e9a8dbad583d64cef7b8fbb0c3 Mon Sep 17 00:00:00 2001 From: Aki Hamano <54422211+t-hamano@users.noreply.github.com> Date: Sun, 15 Sep 2024 01:12:19 +0900 Subject: [PATCH 10/17] Editor: Use hooks instead of HoC in `BlockManager` (#65349) Co-authored-by: t-hamano Co-authored-by: Mamaduka --- .../src/components/block-manager/index.js | 114 ++++++++---------- 1 file changed, 49 insertions(+), 65 deletions(-) diff --git a/packages/editor/src/components/block-manager/index.js b/packages/editor/src/components/block-manager/index.js index b3bc1c6f9b0e8..4a1145839976f 100644 --- a/packages/editor/src/components/block-manager/index.js +++ b/packages/editor/src/components/block-manager/index.js @@ -2,11 +2,11 @@ * WordPress dependencies */ import { store as blocksStore } from '@wordpress/blocks'; -import { withSelect, withDispatch } from '@wordpress/data'; +import { useDispatch, useSelect } from '@wordpress/data'; import { SearchControl, Button } from '@wordpress/components'; import { __, _n, sprintf } from '@wordpress/i18n'; import { useEffect, useState } from '@wordpress/element'; -import { useDebounce, compose } from '@wordpress/compose'; +import { useDebounce } from '@wordpress/compose'; import { speak } from '@wordpress/a11y'; import { store as preferencesStore } from '@wordpress/preferences'; @@ -17,21 +17,48 @@ import { unlock } from '../../lock-unlock'; import { store as editorStore } from '../../store'; import BlockManagerCategory from './category'; -function BlockManager( { - blockTypes, - categories, - hasBlockSupport, - isMatchingSearchTerm, - numberOfHiddenBlocks, - enableAllBlockTypes, -} ) { +export default function BlockManager() { const debouncedSpeak = useDebounce( speak, 500 ); const [ search, setSearch ] = useState( '' ); + const { showBlockTypes } = unlock( useDispatch( editorStore ) ); - // Filtering occurs here (as opposed to `withSelect`) to avoid - // wasted renders by consequence of `Array#filter` producing - // a new value reference on each call. - blockTypes = blockTypes.filter( + const { + blockTypes, + categories, + hasBlockSupport, + isMatchingSearchTerm, + numberOfHiddenBlocks, + } = useSelect( ( select ) => { + // Some hidden blocks become unregistered + // by removing for instance the plugin that registered them, yet + // they're still remain as hidden by the user's action. + // We consider "hidden", blocks which were hidden and + // are still registered. + const _blockTypes = select( blocksStore ).getBlockTypes(); + const hiddenBlockTypes = ( + select( preferencesStore ).get( 'core', 'hiddenBlockTypes' ) ?? [] + ).filter( ( hiddenBlock ) => { + return _blockTypes.some( + ( registeredBlock ) => registeredBlock.name === hiddenBlock + ); + } ); + + return { + blockTypes: _blockTypes, + categories: select( blocksStore ).getCategories(), + hasBlockSupport: select( blocksStore ).hasBlockSupport, + isMatchingSearchTerm: select( blocksStore ).isMatchingSearchTerm, + numberOfHiddenBlocks: + Array.isArray( hiddenBlockTypes ) && hiddenBlockTypes.length, + }; + }, [] ); + + function enableAllBlockTypes( newBlockTypes ) { + const blockNames = newBlockTypes.map( ( { name } ) => name ); + showBlockTypes( blockNames ); + } + + const filteredBlockTypes = blockTypes.filter( ( blockType ) => hasBlockSupport( blockType, 'inserter', true ) && ( ! search || isMatchingSearchTerm( blockType, search ) ) && @@ -44,14 +71,14 @@ function BlockManager( { if ( ! search ) { return; } - const count = blockTypes.length; + const count = filteredBlockTypes.length; const resultsFoundMessage = sprintf( /* translators: %d: number of results. */ _n( '%d result found.', '%d results found.', count ), count ); debouncedSpeak( resultsFoundMessage ); - }, [ blockTypes.length, search, debouncedSpeak ] ); + }, [ filteredBlockTypes?.length, search, debouncedSpeak ] ); return (
@@ -69,7 +96,9 @@ function BlockManager( { @@ -89,7 +118,7 @@ function BlockManager( { aria-label={ __( 'Available block types' ) } className="editor-block-manager__results" > - { blockTypes.length === 0 && ( + { filteredBlockTypes.length === 0 && (

{ __( 'No blocks found.' ) }

@@ -98,7 +127,7 @@ function BlockManager( { blockType.category === category.slug ) } @@ -106,7 +135,7 @@ function BlockManager( { ) ) } ! category ) } /> @@ -114,48 +143,3 @@ function BlockManager( {
); } - -export default compose( [ - withSelect( ( select ) => { - const { - getBlockTypes, - getCategories, - hasBlockSupport, - isMatchingSearchTerm, - } = select( blocksStore ); - const { get } = select( preferencesStore ); - - // Some hidden blocks become unregistered - // by removing for instance the plugin that registered them, yet - // they're still remain as hidden by the user's action. - // We consider "hidden", blocks which were hidden and - // are still registered. - const blockTypes = getBlockTypes(); - const hiddenBlockTypes = ( - get( 'core', 'hiddenBlockTypes' ) ?? [] - ).filter( ( hiddenBlock ) => { - return blockTypes.some( - ( registeredBlock ) => registeredBlock.name === hiddenBlock - ); - } ); - const numberOfHiddenBlocks = - Array.isArray( hiddenBlockTypes ) && hiddenBlockTypes.length; - - return { - blockTypes, - categories: getCategories(), - hasBlockSupport, - isMatchingSearchTerm, - numberOfHiddenBlocks, - }; - } ), - withDispatch( ( dispatch ) => { - const { showBlockTypes } = unlock( dispatch( editorStore ) ); - return { - enableAllBlockTypes: ( blockTypes ) => { - const blockNames = blockTypes.map( ( { name } ) => name ); - showBlockTypes( blockNames ); - }, - }; - } ), -] )( BlockManager ); From 9f7966610ed14a6e00b832a3c97e1d10caf25d7c Mon Sep 17 00:00:00 2001 From: Ramon Date: Mon, 16 Sep 2024 11:33:11 +1000 Subject: [PATCH 11/17] Global Styles: refactor site background controls and move site global styles into Background group (#65304) Expose background styles in the top level global styles navigation menu. Background is now underneath color. Refactor background image controls and their styles into a dedicated component. This is in preparation for adding colors and other background controls later. Co-authored-by: amitraj2203 Co-authored-by: mtias Co-authored-by: mirka <0mirka00@git.wordpress.org> Co-authored-by: tyxla Co-authored-by: ramonjd Co-authored-by: ciampo Co-authored-by: jasmussen Co-authored-by: richtabor Co-authored-by: jameskoster --- .../background-image-control/index.js | 741 +++++++++++++++++ .../background-image-control/style.scss | 170 ++++ .../background-image-control/test/index.js | 47 ++ .../global-styles/background-panel.js | 749 +----------------- .../src/components/global-styles/style.scss | 168 ---- .../global-styles/test/background-panel.js | 48 +- packages/block-editor/src/style.scss | 1 + .../src/components/global-styles/root-menu.js | 16 + .../global-styles/screen-background.js | 37 + .../components/global-styles/screen-layout.js | 16 +- .../src/components/global-styles/ui.js | 5 + packages/icons/src/index.js | 1 + packages/icons/src/library/background.js | 16 + 13 files changed, 1057 insertions(+), 958 deletions(-) create mode 100644 packages/block-editor/src/components/background-image-control/index.js create mode 100644 packages/block-editor/src/components/background-image-control/style.scss create mode 100644 packages/block-editor/src/components/background-image-control/test/index.js create mode 100644 packages/edit-site/src/components/global-styles/screen-background.js create mode 100644 packages/icons/src/library/background.js diff --git a/packages/block-editor/src/components/background-image-control/index.js b/packages/block-editor/src/components/background-image-control/index.js new file mode 100644 index 0000000000000..2703aa3988d64 --- /dev/null +++ b/packages/block-editor/src/components/background-image-control/index.js @@ -0,0 +1,741 @@ +/** + * External dependencies + */ +import clsx from 'clsx'; + +/** + * WordPress dependencies + */ +import { + ToggleControl, + __experimentalToggleGroupControl as ToggleGroupControl, + __experimentalToggleGroupControlOption as ToggleGroupControlOption, + __experimentalUnitControl as UnitControl, + __experimentalVStack as VStack, + DropZone, + FlexItem, + FocalPointPicker, + MenuItem, + VisuallyHidden, + __experimentalItemGroup as ItemGroup, + __experimentalHStack as HStack, + __experimentalTruncate as Truncate, + Dropdown, + Placeholder, + Spinner, + __experimentalDropdownContentWrapper as DropdownContentWrapper, +} from '@wordpress/components'; +import { __, _x, sprintf } from '@wordpress/i18n'; +import { store as noticesStore } from '@wordpress/notices'; +import { getFilename } from '@wordpress/url'; +import { useRef, useState, useEffect, useMemo } from '@wordpress/element'; +import { useDispatch, useSelect } from '@wordpress/data'; +import { focus } from '@wordpress/dom'; +import { isBlobURL } from '@wordpress/blob'; + +/** + * Internal dependencies + */ +import { getResolvedValue } from '../global-styles/utils'; +import { hasBackgroundImageValue } from '../global-styles/background-panel'; +import { setImmutably } from '../../utils/object'; +import MediaReplaceFlow from '../media-replace-flow'; +import { store as blockEditorStore } from '../../store'; + +import { + globalStylesDataKey, + globalStylesLinksDataKey, +} from '../../store/private-keys'; + +const IMAGE_BACKGROUND_TYPE = 'image'; + +const BACKGROUND_POPOVER_PROPS = { + placement: 'left-start', + offset: 36, + shift: true, + className: 'block-editor-global-styles-background-panel__popover', +}; +const noop = () => {}; + +/** + * Get the help text for the background size control. + * + * @param {string} value backgroundSize value. + * @return {string} Translated help text. + */ +function backgroundSizeHelpText( value ) { + if ( value === 'cover' || value === undefined ) { + return __( 'Image covers the space evenly.' ); + } + if ( value === 'contain' ) { + return __( 'Image is contained without distortion.' ); + } + return __( 'Image has a fixed width.' ); +} + +/** + * Converts decimal x and y coords from FocalPointPicker to percentage-based values + * to use as backgroundPosition value. + * + * @param {{x?:number, y?:number}} value FocalPointPicker coords. + * @return {string} backgroundPosition value. + */ +export const coordsToBackgroundPosition = ( value ) => { + if ( ! value || ( isNaN( value.x ) && isNaN( value.y ) ) ) { + return undefined; + } + + const x = isNaN( value.x ) ? 0.5 : value.x; + const y = isNaN( value.y ) ? 0.5 : value.y; + + return `${ x * 100 }% ${ y * 100 }%`; +}; + +/** + * Converts backgroundPosition value to x and y coords for FocalPointPicker. + * + * @param {string} value backgroundPosition value. + * @return {{x?:number, y?:number}} FocalPointPicker coords. + */ +export const backgroundPositionToCoords = ( value ) => { + if ( ! value ) { + return { x: undefined, y: undefined }; + } + + let [ x, y ] = value.split( ' ' ).map( ( v ) => parseFloat( v ) / 100 ); + x = isNaN( x ) ? undefined : x; + y = isNaN( y ) ? x : y; + + return { x, y }; +}; + +function InspectorImagePreviewItem( { + as = 'span', + imgUrl, + toggleProps = {}, + filename, + label, + className, + onToggleCallback = noop, +} ) { + useEffect( () => { + if ( typeof toggleProps?.isOpen !== 'undefined' ) { + onToggleCallback( toggleProps?.isOpen ); + } + }, [ toggleProps?.isOpen, onToggleCallback ] ); + return ( + + + { imgUrl && ( + + + + ) } + + + { label } + + + { imgUrl + ? sprintf( + /* translators: %s: file name */ + __( 'Background image: %s' ), + filename || label + ) + : __( 'No background image selected' ) } + + + + + ); +} + +function BackgroundControlsPanel( { + label, + filename, + url: imgUrl, + children, + onToggle: onToggleCallback = noop, + hasImageValue, +} ) { + if ( ! hasImageValue ) { + return; + } + + const imgLabel = + label || getFilename( imgUrl ) || __( 'Add background image' ); + + return ( + { + const toggleProps = { + onClick: onToggle, + className: + 'block-editor-global-styles-background-panel__dropdown-toggle', + 'aria-expanded': isOpen, + 'aria-label': __( + 'Background size, position and repeat options.' + ), + isOpen, + }; + return ( + + ); + } } + renderContent={ () => ( + + { children } + + ) } + /> + ); +} + +function LoadingSpinner() { + return ( + + + + ); +} + +function BackgroundImageControls( { + onChange, + style, + inheritedValue, + onRemoveImage = noop, + onResetImage = noop, + displayInPanel, + defaultValues, +} ) { + const [ isUploading, setIsUploading ] = useState( false ); + const { getSettings } = useSelect( blockEditorStore ); + + const { id, title, url } = style?.background?.backgroundImage || { + ...inheritedValue?.background?.backgroundImage, + }; + const replaceContainerRef = useRef(); + const { createErrorNotice } = useDispatch( noticesStore ); + const onUploadError = ( message ) => { + createErrorNotice( message, { type: 'snackbar' } ); + setIsUploading( false ); + }; + + const resetBackgroundImage = () => + onChange( + setImmutably( + style, + [ 'background', 'backgroundImage' ], + undefined + ) + ); + + const onSelectMedia = ( media ) => { + if ( ! media || ! media.url ) { + resetBackgroundImage(); + setIsUploading( false ); + return; + } + + if ( isBlobURL( media.url ) ) { + setIsUploading( true ); + return; + } + + // For media selections originated from a file upload. + if ( + ( media.media_type && + media.media_type !== IMAGE_BACKGROUND_TYPE ) || + ( ! media.media_type && + media.type && + media.type !== IMAGE_BACKGROUND_TYPE ) + ) { + onUploadError( + __( 'Only images can be used as a background image.' ) + ); + return; + } + + const sizeValue = + style?.background?.backgroundSize || defaultValues?.backgroundSize; + const positionValue = style?.background?.backgroundPosition; + onChange( + setImmutably( style, [ 'background' ], { + ...style?.background, + backgroundImage: { + url: media.url, + id: media.id, + source: 'file', + 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, + backgroundSize: sizeValue, + } ) + ); + setIsUploading( false ); + }; + + // Drag and drop callback, restricting image to one. + const onFilesDrop = ( filesList ) => { + if ( filesList?.length > 1 ) { + onUploadError( + __( 'Only one image can be used as a background image.' ) + ); + return; + } + getSettings().mediaUpload( { + allowedTypes: [ IMAGE_BACKGROUND_TYPE ], + filesList, + onFileChange( [ image ] ) { + onSelectMedia( image ); + }, + onError: onUploadError, + } ); + }; + + const hasValue = hasBackgroundImageValue( style ); + + const closeAndFocus = () => { + const [ toggleButton ] = focus.tabbable.find( + replaceContainerRef.current + ); + // Focus the toggle button and close the dropdown menu. + // This ensures similar behaviour as to selecting an image, where the dropdown is + // closed and focus is redirected to the dropdown toggle button. + toggleButton?.focus(); + toggleButton?.click(); + }; + + const onRemove = () => + onChange( + setImmutably( style, [ 'background' ], { + backgroundImage: 'none', + } ) + ); + const canRemove = ! hasValue && hasBackgroundImageValue( inheritedValue ); + const imgLabel = + title || getFilename( url ) || __( 'Add background image' ); + + return ( +
+ { isUploading && } + + } + variant="secondary" + onError={ onUploadError } + onReset={ () => { + closeAndFocus(); + onResetImage(); + } } + > + { canRemove && ( + { + closeAndFocus(); + onRemove(); + onRemoveImage(); + } } + > + { __( 'Remove' ) } + + ) } + + +
+ ); +} + +function BackgroundSizeControls( { + onChange, + style, + inheritedValue, + defaultValues, +} ) { + const sizeValue = + style?.background?.backgroundSize || + inheritedValue?.background?.backgroundSize; + const repeatValue = + style?.background?.backgroundRepeat || + inheritedValue?.background?.backgroundRepeat; + const imageValue = + style?.background?.backgroundImage?.url || + inheritedValue?.background?.backgroundImage?.url; + const isUploadedImage = style?.background?.backgroundImage?.id; + const positionValue = + style?.background?.backgroundPosition || + inheritedValue?.background?.backgroundPosition; + const attachmentValue = + style?.background?.backgroundAttachment || + inheritedValue?.background?.backgroundAttachment; + + /* + * Set default values for uploaded images. + * The default values are passed by the consumer. + * Block-level controls may have different defaults to root-level controls. + * A falsy value is treated by default as `auto` (Tile). + */ + 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 + * should reflect the current repeat value. + */ + const repeatCheckedValue = ! ( + repeatValue === 'no-repeat' || + ( currentValueForToggle === 'cover' && repeatValue === undefined ) + ); + + const updateBackgroundSize = ( next ) => { + // When switching to 'contain' toggle the repeat off. + let nextRepeat = repeatValue; + let nextPosition = positionValue; + + if ( next === 'contain' ) { + nextRepeat = 'no-repeat'; + nextPosition = undefined; + } + + if ( next === 'cover' ) { + nextRepeat = undefined; + nextPosition = undefined; + } + + if ( + ( currentValueForToggle === 'cover' || + currentValueForToggle === 'contain' ) && + next === 'auto' + ) { + nextRepeat = undefined; + /* + * A background image uploaded and set in the editor (an image with a record id), + * 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'; + } + } + + /* + * Next will be null when the input is cleared, + * in which case the value should be 'auto'. + */ + if ( ! next && currentValueForToggle === 'auto' ) { + next = 'auto'; + } + + onChange( + setImmutably( style, [ 'background' ], { + ...style?.background, + backgroundPosition: nextPosition, + backgroundRepeat: nextRepeat, + backgroundSize: next, + } ) + ); + }; + + const updateBackgroundPosition = ( next ) => { + onChange( + setImmutably( + style, + [ 'background', 'backgroundPosition' ], + coordsToBackgroundPosition( next ) + ) + ); + }; + + const toggleIsRepeated = () => + onChange( + setImmutably( + style, + [ 'background', 'backgroundRepeat' ], + repeatCheckedValue === true ? 'no-repeat' : 'repeat' + ) + ); + + const toggleScrollWithPage = () => + onChange( + setImmutably( + style, + [ 'background', 'backgroundAttachment' ], + attachmentValue === 'fixed' ? 'scroll' : 'fixed' + ) + ); + + // 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 ( + + + + + + + + + + + + + + ); +} + +export default function BackgroundImagePanel( { + value, + onChange, + inheritedValue = value, + settings, + defaultValues = {}, +} ) { + /* + * 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 resetBackground = () => + onChange( setImmutably( value, [ 'background' ], {} ) ); + + const { title, url } = value?.background?.backgroundImage || { + ...resolvedInheritedValue?.background?.backgroundImage, + }; + const hasImageValue = + hasBackgroundImageValue( value ) || + hasBackgroundImageValue( resolvedInheritedValue ); + + const imageValue = + value?.background?.backgroundImage || + inheritedValue?.background?.backgroundImage; + + const shouldShowBackgroundImageControls = + hasImageValue && + 'none' !== imageValue && + ( settings?.background?.backgroundSize || + settings?.background?.backgroundPosition || + settings?.background?.backgroundRepeat ); + + const [ isDropDownOpen, setIsDropDownOpen ] = useState( false ); + + return ( +
+ { shouldShowBackgroundImageControls ? ( + + + { + setIsDropDownOpen( false ); + resetBackground(); + } } + onRemoveImage={ () => setIsDropDownOpen( false ) } + defaultValues={ defaultValues } + /> + + + + ) : ( + { + setIsDropDownOpen( false ); + resetBackground(); + } } + onRemoveImage={ () => setIsDropDownOpen( false ) } + /> + ) } +
+ ); +} diff --git a/packages/block-editor/src/components/background-image-control/style.scss b/packages/block-editor/src/components/background-image-control/style.scss new file mode 100644 index 0000000000000..cde8044c24c12 --- /dev/null +++ b/packages/block-editor/src/components/background-image-control/style.scss @@ -0,0 +1,170 @@ +.block-editor-global-styles-background-panel__inspector-media-replace-container { + border: $border-width solid $gray-300; + border-radius: $radius-small; + // Full width. ToolsPanel lays out children in a grid. + grid-column: 1 / -1; + + &.is-open { + background-color: $gray-100; + } + + .block-editor-global-styles-background-panel__image-tools-panel-item { + flex-grow: 1; + border: 0; + + .components-dropdown { + display: block; + } + } + + .block-editor-global-styles-background-panel__inspector-preview-inner { + height: 100%; + } + + .components-dropdown { + display: block; + height: 36px; + } +} + +.block-editor-global-styles-background-panel__image-tools-panel-item { + border: $border-width solid $gray-300; + + // Full width. ToolsPanel lays out children in a grid. + grid-column: 1 / -1; + + // Ensure the dropzone is positioned to the size of the item. + position: relative; + + // Since there is no option to skip rendering the drag'n'drop icon in drop + // zone, we hide it for now. + .components-drop-zone__content-icon { + display: none; + } + + .components-dropdown { + display: block; + height: 36px; + } + + button.components-button { + color: $gray-900; + width: 100%; + display: block; + + &:hover { + color: var(--wp-admin-theme-color); + } + + &:focus { + box-shadow: inset 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); + } + } + + .block-editor-global-styles-background-panel__loading { + height: 100%; + position: absolute; + z-index: 1; + width: 100%; + padding: 10px 0 0 0; + + svg { + margin: 0; + } + } +} + +.block-editor-global-styles-background-panel__image-preview-content, +.block-editor-global-styles-background-panel__dropdown-toggle { + height: 100%; + width: 100%; + padding-left: $grid-unit-15; +} + +.block-editor-global-styles-background-panel__dropdown-toggle { + cursor: pointer; + background: transparent; + border: none; +} + +.block-editor-global-styles-background-panel__inspector-media-replace-title { + word-break: break-all; + // The Button component is white-space: nowrap, and that won't work with line-clamp. + white-space: normal; + + // Without this, the ellipsis can sometimes be partially hidden by the Button padding. + text-align: start; + text-align-last: center; +} + +.block-editor-global-styles-background-panel__inspector-preview-inner { + .block-editor-global-styles-background-panel__inspector-image-indicator-wrapper { + width: 20px; + height: 20px; + min-width: auto; + } +} + +.block-editor-global-styles-background-panel__inspector-image-indicator { + background-size: cover; + border-radius: $radius-round; + width: 20px; + height: 20px; + display: block; + position: relative; +} + +.block-editor-global-styles-background-panel__inspector-image-indicator::after { + content: ""; + position: absolute; + top: -1px; + left: -1px; + bottom: -1px; + right: -1px; + border-radius: $radius-round; + box-shadow: inset 0 0 0 $border-width rgba(0, 0, 0, 0.2); + // Show a thin outline in Windows high contrast mode, otherwise the button is invisible. + border: 1px solid transparent; + box-sizing: inherit; +} + +.block-editor-global-styles-background-panel__dropdown-content-wrapper { + min-width: 260px; + overflow-x: hidden; + + .components-focal-point-picker-wrapper { + background-color: $gray-100; + width: 100%; + border-radius: $radius-small; + border: $border-width solid $gray-300; + } + + .components-focal-point-picker__media--image { + max-height: 180px; + } + + // Override focal picker to avoid a double border. + .components-focal-point-picker::after { + content: none; + } +} + +// Push control panel into the background when the media modal is open. +.modal-open .block-editor-global-styles-background-panel__popover { + z-index: z-index(".block-editor-global-styles-background-panel__popover"); +} + +.block-editor-global-styles-background-panel__media-replace-popover { + .components-popover__content { + // width of block-editor-global-styles-background-panel__dropdown-content-wrapper minus padding. + width: 226px; + } + + .components-button { + padding: 0 $grid-unit-10; + } + + .components-button .components-menu-items__item-icon.has-icon-right { + margin-left: $grid-unit-30 - $grid-unit-10; + } +} diff --git a/packages/block-editor/src/components/background-image-control/test/index.js b/packages/block-editor/src/components/background-image-control/test/index.js new file mode 100644 index 0000000000000..ebadad97eda02 --- /dev/null +++ b/packages/block-editor/src/components/background-image-control/test/index.js @@ -0,0 +1,47 @@ +/** + * Internal dependencies + */ + +import { backgroundPositionToCoords, coordsToBackgroundPosition } from '../'; + +describe( 'backgroundPositionToCoords', () => { + it( 'should return the correct coordinates for a percentage value using 2-value syntax', () => { + expect( backgroundPositionToCoords( '25% 75%' ) ).toEqual( { + x: 0.25, + y: 0.75, + } ); + } ); + + it( 'should return the correct coordinates for a percentage using 1-value syntax', () => { + expect( backgroundPositionToCoords( '50%' ) ).toEqual( { + x: 0.5, + y: 0.5, + } ); + } ); + + it( 'should return undefined coords in given an empty value', () => { + expect( backgroundPositionToCoords( '' ) ).toEqual( { + x: undefined, + y: undefined, + } ); + } ); + + it( 'should return undefined coords in given a string that cannot be converted', () => { + expect( backgroundPositionToCoords( 'apples' ) ).toEqual( { + x: undefined, + y: undefined, + } ); + } ); +} ); + +describe( 'coordsToBackgroundPosition', () => { + it( 'should return the correct background position for a set of coordinates', () => { + expect( coordsToBackgroundPosition( { x: 0.25, y: 0.75 } ) ).toBe( + '25% 75%' + ); + } ); + + it( 'should return undefined if no coordinates are provided', () => { + expect( coordsToBackgroundPosition( {} ) ).toBeUndefined(); + } ); +} ); 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 93fed5780e454..c66ea01bce549 100644 --- a/packages/block-editor/src/components/global-styles/background-panel.js +++ b/packages/block-editor/src/components/global-styles/background-panel.js @@ -1,71 +1,22 @@ -/** - * External dependencies - */ -import clsx from 'clsx'; - /** * WordPress dependencies */ import { __experimentalToolsPanel as ToolsPanel, __experimentalToolsPanelItem as ToolsPanelItem, - ToggleControl, - __experimentalToggleGroupControl as ToggleGroupControl, - __experimentalToggleGroupControlOption as ToggleGroupControlOption, - __experimentalUnitControl as UnitControl, - __experimentalVStack as VStack, - DropZone, - FlexItem, - FocalPointPicker, - MenuItem, - VisuallyHidden, - __experimentalItemGroup as ItemGroup, - __experimentalHStack as HStack, - __experimentalTruncate as Truncate, - Dropdown, - Placeholder, - Spinner, - __experimentalDropdownContentWrapper as DropdownContentWrapper, } from '@wordpress/components'; -import { __, _x, sprintf } from '@wordpress/i18n'; -import { store as noticesStore } from '@wordpress/notices'; -import { getFilename } from '@wordpress/url'; -import { - useCallback, - Platform, - useRef, - useState, - useEffect, - useMemo, -} from '@wordpress/element'; -import { useDispatch, useSelect } from '@wordpress/data'; -import { focus } from '@wordpress/dom'; -import { isBlobURL } from '@wordpress/blob'; - +import { useCallback, Platform } from '@wordpress/element'; /** * Internal dependencies */ -import { useToolsPanelDropdownMenuProps, getResolvedValue } from './utils'; +import BackgroundImageControl from '../background-image-control'; +import { useToolsPanelDropdownMenuProps } from './utils'; import { setImmutably } from '../../utils/object'; -import MediaReplaceFlow from '../media-replace-flow'; -import { store as blockEditorStore } from '../../store'; - -import { - globalStylesDataKey, - globalStylesLinksDataKey, -} from '../../store/private-keys'; +import { __ } from '@wordpress/i18n'; -const IMAGE_BACKGROUND_TYPE = 'image'; const DEFAULT_CONTROLS = { backgroundImage: true, }; -const BACKGROUND_POPOVER_PROPS = { - placement: 'left-start', - offset: 36, - shift: true, - className: 'block-editor-global-styles-background-panel__popover', -}; -const noop = () => {}; /** * Checks site settings to see if the background panel may be used. @@ -110,567 +61,6 @@ export function hasBackgroundImageValue( style ) { ); } -/** - * Get the help text for the background size control. - * - * @param {string} value backgroundSize value. - * @return {string} Translated help text. - */ -function backgroundSizeHelpText( value ) { - if ( value === 'cover' || value === undefined ) { - return __( 'Image covers the space evenly.' ); - } - if ( value === 'contain' ) { - return __( 'Image is contained without distortion.' ); - } - return __( 'Image has a fixed width.' ); -} - -/** - * Converts decimal x and y coords from FocalPointPicker to percentage-based values - * to use as backgroundPosition value. - * - * @param {{x?:number, y?:number}} value FocalPointPicker coords. - * @return {string} backgroundPosition value. - */ -export const coordsToBackgroundPosition = ( value ) => { - if ( ! value || ( isNaN( value.x ) && isNaN( value.y ) ) ) { - return undefined; - } - - const x = isNaN( value.x ) ? 0.5 : value.x; - const y = isNaN( value.y ) ? 0.5 : value.y; - - return `${ x * 100 }% ${ y * 100 }%`; -}; - -/** - * Converts backgroundPosition value to x and y coords for FocalPointPicker. - * - * @param {string} value backgroundPosition value. - * @return {{x?:number, y?:number}} FocalPointPicker coords. - */ -export const backgroundPositionToCoords = ( value ) => { - if ( ! value ) { - return { x: undefined, y: undefined }; - } - - let [ x, y ] = value.split( ' ' ).map( ( v ) => parseFloat( v ) / 100 ); - x = isNaN( x ) ? undefined : x; - y = isNaN( y ) ? x : y; - - return { x, y }; -}; - -function InspectorImagePreviewItem( { - as = 'span', - imgUrl, - toggleProps = {}, - filename, - label, - className, - onToggleCallback = noop, -} ) { - useEffect( () => { - if ( typeof toggleProps?.isOpen !== 'undefined' ) { - onToggleCallback( toggleProps?.isOpen ); - } - }, [ toggleProps?.isOpen, onToggleCallback ] ); - return ( - - - { imgUrl && ( - - - - ) } - - - { label } - - - { imgUrl - ? sprintf( - /* translators: %s: file name */ - __( 'Background image: %s' ), - filename || label - ) - : __( 'No background image selected' ) } - - - - - ); -} - -function BackgroundControlsPanel( { - label, - filename, - url: imgUrl, - children, - onToggle: onToggleCallback = noop, - hasImageValue, -} ) { - if ( ! hasImageValue ) { - return; - } - - const imgLabel = - label || getFilename( imgUrl ) || __( 'Add background image' ); - - return ( - { - const toggleProps = { - onClick: onToggle, - className: - 'block-editor-global-styles-background-panel__dropdown-toggle', - 'aria-expanded': isOpen, - 'aria-label': __( - 'Background size, position and repeat options.' - ), - isOpen, - }; - return ( - - ); - } } - renderContent={ () => ( - - { children } - - ) } - /> - ); -} - -function LoadingSpinner() { - return ( - - - - ); -} - -function BackgroundImageControls( { - onChange, - style, - inheritedValue, - onRemoveImage = noop, - onResetImage = noop, - displayInPanel, - defaultValues, -} ) { - const [ isUploading, setIsUploading ] = useState( false ); - const { getSettings } = useSelect( blockEditorStore ); - - const { id, title, url } = style?.background?.backgroundImage || { - ...inheritedValue?.background?.backgroundImage, - }; - const replaceContainerRef = useRef(); - const { createErrorNotice } = useDispatch( noticesStore ); - const onUploadError = ( message ) => { - createErrorNotice( message, { type: 'snackbar' } ); - setIsUploading( false ); - }; - - const resetBackgroundImage = () => - onChange( - setImmutably( - style, - [ 'background', 'backgroundImage' ], - undefined - ) - ); - - const onSelectMedia = ( media ) => { - if ( ! media || ! media.url ) { - resetBackgroundImage(); - setIsUploading( false ); - return; - } - - if ( isBlobURL( media.url ) ) { - setIsUploading( true ); - return; - } - - // For media selections originated from a file upload. - if ( - ( media.media_type && - media.media_type !== IMAGE_BACKGROUND_TYPE ) || - ( ! media.media_type && - media.type && - media.type !== IMAGE_BACKGROUND_TYPE ) - ) { - onUploadError( - __( 'Only images can be used as a background image.' ) - ); - return; - } - - const sizeValue = - style?.background?.backgroundSize || defaultValues?.backgroundSize; - const positionValue = style?.background?.backgroundPosition; - onChange( - setImmutably( style, [ 'background' ], { - ...style?.background, - backgroundImage: { - url: media.url, - id: media.id, - source: 'file', - 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, - backgroundSize: sizeValue, - } ) - ); - setIsUploading( false ); - }; - - // Drag and drop callback, restricting image to one. - const onFilesDrop = ( filesList ) => { - if ( filesList?.length > 1 ) { - onUploadError( - __( 'Only one image can be used as a background image.' ) - ); - return; - } - getSettings().mediaUpload( { - allowedTypes: [ IMAGE_BACKGROUND_TYPE ], - filesList, - onFileChange( [ image ] ) { - onSelectMedia( image ); - }, - onError: onUploadError, - } ); - }; - - const hasValue = hasBackgroundImageValue( style ); - - const closeAndFocus = () => { - const [ toggleButton ] = focus.tabbable.find( - replaceContainerRef.current - ); - // Focus the toggle button and close the dropdown menu. - // This ensures similar behaviour as to selecting an image, where the dropdown is - // closed and focus is redirected to the dropdown toggle button. - toggleButton?.focus(); - toggleButton?.click(); - }; - - const onRemove = () => - onChange( - setImmutably( style, [ 'background' ], { - backgroundImage: 'none', - } ) - ); - const canRemove = ! hasValue && hasBackgroundImageValue( inheritedValue ); - const imgLabel = - title || getFilename( url ) || __( 'Add background image' ); - - return ( -
- { isUploading && } - - } - variant="secondary" - onError={ onUploadError } - onReset={ () => { - closeAndFocus(); - onResetImage(); - } } - > - { canRemove && ( - { - closeAndFocus(); - onRemove(); - onRemoveImage(); - } } - > - { __( 'Remove' ) } - - ) } - - -
- ); -} - -function BackgroundSizeControls( { - onChange, - style, - inheritedValue, - defaultValues, -} ) { - const sizeValue = - style?.background?.backgroundSize || - inheritedValue?.background?.backgroundSize; - const repeatValue = - style?.background?.backgroundRepeat || - inheritedValue?.background?.backgroundRepeat; - const imageValue = - style?.background?.backgroundImage?.url || - inheritedValue?.background?.backgroundImage?.url; - const isUploadedImage = style?.background?.backgroundImage?.id; - const positionValue = - style?.background?.backgroundPosition || - inheritedValue?.background?.backgroundPosition; - const attachmentValue = - style?.background?.backgroundAttachment || - inheritedValue?.background?.backgroundAttachment; - - /* - * Set default values for uploaded images. - * The default values are passed by the consumer. - * Block-level controls may have different defaults to root-level controls. - * A falsy value is treated by default as `auto` (Tile). - */ - 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 - * should reflect the current repeat value. - */ - const repeatCheckedValue = ! ( - repeatValue === 'no-repeat' || - ( currentValueForToggle === 'cover' && repeatValue === undefined ) - ); - - const updateBackgroundSize = ( next ) => { - // When switching to 'contain' toggle the repeat off. - let nextRepeat = repeatValue; - let nextPosition = positionValue; - - if ( next === 'contain' ) { - nextRepeat = 'no-repeat'; - nextPosition = undefined; - } - - if ( next === 'cover' ) { - nextRepeat = undefined; - nextPosition = undefined; - } - - if ( - ( currentValueForToggle === 'cover' || - currentValueForToggle === 'contain' ) && - next === 'auto' - ) { - nextRepeat = undefined; - /* - * A background image uploaded and set in the editor (an image with a record id), - * 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'; - } - } - - /* - * Next will be null when the input is cleared, - * in which case the value should be 'auto'. - */ - if ( ! next && currentValueForToggle === 'auto' ) { - next = 'auto'; - } - - onChange( - setImmutably( style, [ 'background' ], { - ...style?.background, - backgroundPosition: nextPosition, - backgroundRepeat: nextRepeat, - backgroundSize: next, - } ) - ); - }; - - const updateBackgroundPosition = ( next ) => { - onChange( - setImmutably( - style, - [ 'background', 'backgroundPosition' ], - coordsToBackgroundPosition( next ) - ) - ); - }; - - const toggleIsRepeated = () => - onChange( - setImmutably( - style, - [ 'background', 'backgroundRepeat' ], - repeatCheckedValue === true ? 'no-repeat' : 'repeat' - ) - ); - - const toggleScrollWithPage = () => - onChange( - setImmutably( - style, - [ 'background', 'backgroundAttachment' ], - attachmentValue === 'fixed' ? 'scroll' : 'fixed' - ) - ); - - // 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 ( - - - - - - - - - - - - - - ); -} - function BackgroundToolsPanel( { resetAllFilter, onChange, @@ -697,54 +87,20 @@ function BackgroundToolsPanel( { ); } -export default function BackgroundPanel( { +export default function BackgroundImagePanel( { as: Wrapper = BackgroundToolsPanel, value, onChange, - inheritedValue = value, + inheritedValue, settings, panelId, defaultControls = DEFAULT_CONTROLS, defaultValues = {}, headerLabel = __( 'Background image' ), } ) { - /* - * 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 showBackgroundImageControl = useHasBackgroundPanel( settings ); + const resetBackground = () => + onChange( setImmutably( value, [ 'background' ], {} ) ); const resetAllFilter = useCallback( ( previousValue ) => { return { ...previousValue, @@ -752,29 +108,6 @@ export default function BackgroundPanel( { }; }, [] ); - const resetBackground = () => - onChange( setImmutably( value, [ 'background' ], {} ) ); - - const { title, url } = value?.background?.backgroundImage || { - ...resolvedInheritedValue?.background?.backgroundImage, - }; - const hasImageValue = - hasBackgroundImageValue( value ) || - hasBackgroundImageValue( resolvedInheritedValue ); - - const imageValue = - value?.background?.backgroundImage || - inheritedValue?.background?.backgroundImage; - - const shouldShowBackgroundImageControls = - hasImageValue && - 'none' !== imageValue && - ( settings?.background?.backgroundSize || - settings?.background?.backgroundPosition || - settings?.background?.backgroundRepeat ); - - const [ isDropDownOpen, setIsDropDownOpen ] = useState( false ); - return ( -
+ { showBackgroundImageControl && ( !! value?.background } label={ __( 'Image' ) } @@ -798,53 +124,16 @@ export default function BackgroundPanel( { isShownByDefault={ defaultControls.backgroundImage } panelId={ panelId } > - { shouldShowBackgroundImageControls ? ( - - - { - setIsDropDownOpen( false ); - resetBackground(); - } } - onRemoveImage={ () => - setIsDropDownOpen( false ) - } - defaultValues={ defaultValues } - /> - - - - ) : ( - { - setIsDropDownOpen( false ); - resetBackground(); - } } - onRemoveImage={ () => setIsDropDownOpen( false ) } - /> - ) } + -
+ ) }
); } diff --git a/packages/block-editor/src/components/global-styles/style.scss b/packages/block-editor/src/components/global-styles/style.scss index 1cebbfe7a85d4..3ba4f81d09daf 100644 --- a/packages/block-editor/src/components/global-styles/style.scss +++ b/packages/block-editor/src/components/global-styles/style.scss @@ -70,171 +70,3 @@ /*rtl:ignore*/ direction: ltr; } - - -.block-editor-global-styles-background-panel__inspector-media-replace-container { - border: $border-width solid $gray-300; - border-radius: $radius-small; - // Full width. ToolsPanel lays out children in a grid. - grid-column: 1 / -1; - - &.is-open { - background-color: $gray-100; - } - - .block-editor-global-styles-background-panel__image-tools-panel-item { - flex-grow: 1; - border: 0; - .components-dropdown { - display: block; - } - } - - .block-editor-global-styles-background-panel__inspector-preview-inner { - height: 100%; - } - - .components-dropdown { - display: block; - height: 36px; - } -} - -.block-editor-global-styles-background-panel__image-tools-panel-item { - border: $border-width solid $gray-300; - - // Full width. ToolsPanel lays out children in a grid. - grid-column: 1 / -1; - - // Ensure the dropzone is positioned to the size of the item. - position: relative; - - // Since there is no option to skip rendering the drag'n'drop icon in drop - // zone, we hide it for now. - .components-drop-zone__content-icon { - display: none; - } - - .components-dropdown { - display: block; - height: 36px; - } - - button.components-button { - color: $gray-900; - width: 100%; - display: block; - - &:hover { - color: var(--wp-admin-theme-color); - } - - &:focus { - box-shadow: inset 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); - } - } - - .block-editor-global-styles-background-panel__loading { - height: 100%; - position: absolute; - z-index: 1; - width: 100%; - padding: 10px 0 0 0; - svg { - margin: 0; - } - } -} - -.block-editor-global-styles-background-panel__image-preview-content, -.block-editor-global-styles-background-panel__dropdown-toggle { - height: 100%; - width: 100%; - padding-left: $grid-unit-15; -} - -.block-editor-global-styles-background-panel__dropdown-toggle { - cursor: pointer; - background: transparent; - border: none; -} - -.block-editor-global-styles-background-panel__inspector-media-replace-title { - word-break: break-all; - // The Button component is white-space: nowrap, and that won't work with line-clamp. - white-space: normal; - - // Without this, the ellipsis can sometimes be partially hidden by the Button padding. - text-align: start; - text-align-last: center; -} - -.block-editor-global-styles-background-panel__inspector-preview-inner { - .block-editor-global-styles-background-panel__inspector-image-indicator-wrapper { - width: 20px; - height: 20px; - min-width: auto; - } -} - -.block-editor-global-styles-background-panel__inspector-image-indicator { - background-size: cover; - border-radius: $radius-round; - width: 20px; - height: 20px; - display: block; - position: relative; -} - -.block-editor-global-styles-background-panel__inspector-image-indicator::after { - content: ""; - position: absolute; - top: -1px; - left: -1px; - bottom: -1px; - right: -1px; - border-radius: $radius-round; - box-shadow: inset 0 0 0 $border-width rgba(0, 0, 0, 0.2); - // Show a thin outline in Windows high contrast mode, otherwise the button is invisible. - border: 1px solid transparent; - box-sizing: inherit; -} - -.block-editor-global-styles-background-panel__dropdown-content-wrapper { - min-width: 260px; - overflow-x: hidden; - - .components-focal-point-picker-wrapper { - background-color: $gray-100; - width: 100%; - border-radius: $radius-small; - border: $border-width solid $gray-300; - } - - .components-focal-point-picker__media--image { - max-height: 180px; - } - - // Override focal picker to avoid a double border. - .components-focal-point-picker::after { - content: none; - } -} - -// Push control panel into the background when the media modal is open. -.modal-open .block-editor-global-styles-background-panel__popover { - z-index: z-index(".block-editor-global-styles-background-panel__popover"); -} - -.block-editor-global-styles-background-panel__media-replace-popover { - .components-popover__content { - // width of block-editor-global-styles-background-panel__dropdown-content-wrapper minus padding. - width: 226px; - } - .components-button { - padding: 0 $grid-unit-10; - } - .components-button .components-menu-items__item-icon.has-icon-right { - margin-left: $grid-unit-30 - $grid-unit-10; - } -} diff --git a/packages/block-editor/src/components/global-styles/test/background-panel.js b/packages/block-editor/src/components/global-styles/test/background-panel.js index d0b3a8ad60170..ad2c55e747f70 100644 --- a/packages/block-editor/src/components/global-styles/test/background-panel.js +++ b/packages/block-editor/src/components/global-styles/test/background-panel.js @@ -2,53 +2,7 @@ * Internal dependencies */ -import { - backgroundPositionToCoords, - coordsToBackgroundPosition, - hasBackgroundImageValue, -} from '../background-panel'; - -describe( 'backgroundPositionToCoords', () => { - it( 'should return the correct coordinates for a percentage value using 2-value syntax', () => { - expect( backgroundPositionToCoords( '25% 75%' ) ).toEqual( { - x: 0.25, - y: 0.75, - } ); - } ); - - it( 'should return the correct coordinates for a percentage using 1-value syntax', () => { - expect( backgroundPositionToCoords( '50%' ) ).toEqual( { - x: 0.5, - y: 0.5, - } ); - } ); - - it( 'should return undefined coords in given an empty value', () => { - expect( backgroundPositionToCoords( '' ) ).toEqual( { - x: undefined, - y: undefined, - } ); - } ); - - it( 'should return undefined coords in given a string that cannot be converted', () => { - expect( backgroundPositionToCoords( 'apples' ) ).toEqual( { - x: undefined, - y: undefined, - } ); - } ); -} ); - -describe( 'coordsToBackgroundPosition', () => { - it( 'should return the correct background position for a set of coordinates', () => { - expect( coordsToBackgroundPosition( { x: 0.25, y: 0.75 } ) ).toBe( - '25% 75%' - ); - } ); - - it( 'should return undefined if no coordinates are provided', () => { - expect( coordsToBackgroundPosition( {} ) ).toBeUndefined(); - } ); -} ); +import { hasBackgroundImageValue } from '../background-panel'; describe( 'hasBackgroundImageValue', () => { it( 'should return `true` when id and url exist', () => { diff --git a/packages/block-editor/src/style.scss b/packages/block-editor/src/style.scss index feaabbbda9442..e6ec77b55a0ec 100644 --- a/packages/block-editor/src/style.scss +++ b/packages/block-editor/src/style.scss @@ -1,4 +1,5 @@ @import "./autocompleters/style.scss"; +@import "./components/background-image-control/style.scss"; @import "./components/block-alignment-control/style.scss"; @import "./components/block-canvas/style.scss"; @import "./components/block-icon/style.scss"; diff --git a/packages/edit-site/src/components/global-styles/root-menu.js b/packages/edit-site/src/components/global-styles/root-menu.js index 6db4621299f1e..183686bb52d82 100644 --- a/packages/edit-site/src/components/global-styles/root-menu.js +++ b/packages/edit-site/src/components/global-styles/root-menu.js @@ -3,6 +3,7 @@ */ import { __experimentalItemGroup as ItemGroup } from '@wordpress/components'; import { + background, typography, color, layout, @@ -23,11 +24,17 @@ const { useHasColorPanel, useGlobalSetting, useSettingsForBlockElement, + useHasBackgroundPanel, } = unlock( blockEditorPrivateApis ); function RootMenu() { const [ rawSettings ] = useGlobalSetting( '' ); const settings = useSettingsForBlockElement( rawSettings ); + /* + * Use the raw settings to determine if the background panel should be displayed, + * as the background panel is not dependent on the block element settings. + */ + const hasBackgroundPanel = useHasBackgroundPanel( rawSettings ); const hasTypographyPanel = useHasTypographyPanel( settings ); const hasColorPanel = useHasColorPanel( settings ); const hasShadowPanel = true; // useHasShadowPanel( settings ); @@ -55,6 +62,15 @@ function RootMenu() { { __( 'Colors' ) } ) } + { hasBackgroundPanel && ( + + { __( 'Background' ) } + + ) } { hasShadowPanel && ( + + { __( 'Set styles for the site’s background.' ) } + + } + /> + { hasBackgroundPanel && } + + ); +} + +export default ScreenBackground; diff --git a/packages/edit-site/src/components/global-styles/screen-layout.js b/packages/edit-site/src/components/global-styles/screen-layout.js index 1e68309fe0186..b6fa9f18f18de 100644 --- a/packages/edit-site/src/components/global-styles/screen-layout.js +++ b/packages/edit-site/src/components/global-styles/screen-layout.js @@ -8,31 +8,21 @@ import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; * Internal dependencies */ import DimensionsPanel from './dimensions-panel'; -import BackgroundPanel from './background-panel'; import ScreenHeader from './header'; import { unlock } from '../../lock-unlock'; -const { - useHasBackgroundPanel, - useHasDimensionsPanel, - useGlobalSetting, - useSettingsForBlockElement, -} = unlock( blockEditorPrivateApis ); +const { useHasDimensionsPanel, useGlobalSetting, useSettingsForBlockElement } = + unlock( blockEditorPrivateApis ); function ScreenLayout() { const [ rawSettings ] = useGlobalSetting( '' ); const settings = useSettingsForBlockElement( rawSettings ); const hasDimensionsPanel = useHasDimensionsPanel( settings ); - /* - * Use the raw settings to determine if the background panel should be displayed, - * as the background panel is not dependent on the block element settings. - */ - const hasBackgroundPanel = useHasBackgroundPanel( rawSettings ); + return ( <> { hasDimensionsPanel && } - { hasBackgroundPanel && } ); } diff --git a/packages/edit-site/src/components/global-styles/ui.js b/packages/edit-site/src/components/global-styles/ui.js index 54bd4f97390a8..60d7e314d7776 100644 --- a/packages/edit-site/src/components/global-styles/ui.js +++ b/packages/edit-site/src/components/global-styles/ui.js @@ -38,6 +38,7 @@ import FontSize from './font-sizes/font-size'; import FontSizes from './font-sizes/font-sizes'; import ScreenColors from './screen-colors'; import ScreenColorPalette from './screen-color-palette'; +import ScreenBackground from './screen-background'; import { ScreenShadows, ScreenShadowsEdit } from './screen-shadows'; import ScreenLayout from './screen-layout'; import ScreenStyleVariations from './screen-style-variations'; @@ -372,6 +373,10 @@ function GlobalStylesUI() { + + + + { blocks.map( ( block ) => ( + + +); + +export default background; From 45d33f076ee64737b67cea84f2c405cc194b2543 Mon Sep 17 00:00:00 2001 From: Carolina Nymark Date: Mon, 16 Sep 2024 04:57:28 +0200 Subject: [PATCH 12/17] Query loop / Post template: Enable post format filter (#64167) Enables filtering the query loop result by post format. To allow querying a given post format, a new parameter, `format` is added to a new compatibility class called `Gutenberg_REST_Posts_Controller_6_7`, which extends the core class `WP_REST_Posts_Controller`. Query loop block: - Adds a new parameter, `format`, to the `query` attribute in block.json. - Adds a new `FormatControls` that uses a list of supported post formats inside a `FormTokenField`. This control is placed in the `Filters` panel in the block settings sidebar. - Adds a new utility function `gutenberg_add_format_query_vars_to_query_loop_block` to ensure that the Query loop block can pass the new `format` argument correctly to `WP_Query`. This function is hooked into `query_loop_block_query_vars`. Post Template block: The new `format` parameter is passed from the query loop block to the post template block as part of the `query` attribute. --------- Co-authored-by: carolinan Co-authored-by: Mamaduka Co-authored-by: youknowriad Co-authored-by: ntsekouras Co-authored-by: dmsnell Co-authored-by: TimothyBJacobs Co-authored-by: nickbohle Co-authored-by: SergeyBiryukov Co-authored-by: justintadlock --- backport-changelog/6.7/7314.md | 3 + lib/compat/wordpress-6.7/blocks.php | 60 ++ ...ss-gutenberg-rest-posts-controller-6-7.php | 698 ++++++++++++++++++ lib/compat/wordpress-6.7/post-formats.php | 24 + lib/load.php | 2 + .../block-library/src/post-template/edit.js | 6 + packages/block-library/src/query/block.json | 3 +- .../inspector-controls/format-controls.js | 90 +++ .../query/edit/inspector-controls/index.js | 57 +- packages/block-library/src/query/utils.js | 17 +- .../fixtures/blocks/core__query.json | 3 +- 11 files changed, 958 insertions(+), 5 deletions(-) create mode 100644 backport-changelog/6.7/7314.md create mode 100644 lib/compat/wordpress-6.7/class-gutenberg-rest-posts-controller-6-7.php create mode 100644 lib/compat/wordpress-6.7/post-formats.php create mode 100644 packages/block-library/src/query/edit/inspector-controls/format-controls.js diff --git a/backport-changelog/6.7/7314.md b/backport-changelog/6.7/7314.md new file mode 100644 index 0000000000000..7d75cdff0f907 --- /dev/null +++ b/backport-changelog/6.7/7314.md @@ -0,0 +1,3 @@ +https://github.com/WordPress/wordpress-develop/pull/7314 + +* https://github.com/WordPress/gutenberg/pull/64167 diff --git a/lib/compat/wordpress-6.7/blocks.php b/lib/compat/wordpress-6.7/blocks.php index 18d21621be719..6b9526f8056fd 100644 --- a/lib/compat/wordpress-6.7/blocks.php +++ b/lib/compat/wordpress-6.7/blocks.php @@ -43,3 +43,63 @@ function gutenberg_filter_block_type_metadata_settings_allow_variations_php_file return $settings; } add_filter( 'block_type_metadata_settings', 'gutenberg_filter_block_type_metadata_settings_allow_variations_php_file', 10, 2 ); + +/** + * Adds post format query vars to the query loop block's WP_Query when the block's attributes call for them. + * + * @see 'query_loop_block_query_vars' + * + * @param array $query The query vars. + * @param WP_Block $block Block instance. + * @return array The filtered query vars. + */ +function gutenberg_add_format_query_vars_to_query_loop_block( $query, $block ) { + // Return early if there is no format or if the format is not an array. + if ( empty( $block->context['query']['format'] ) || ! is_array( $block->context['query']['format'] ) ) { + return $query; + } + + $formats = $block->context['query']['format']; + $tax_query = array( 'relation' => 'OR' ); + + // The default post format, 'standard', is not stored in the database. + // If 'standard' is part of the request, the query needs to exclude all post items that + // have a format assigned. + if ( in_array( 'standard', $formats, true ) ) { + $tax_query[] = array( + 'taxonomy' => 'post_format', + 'field' => 'slug', + 'terms' => array(), + 'operator' => 'NOT EXISTS', + ); + // Remove the standard format, since it cannot be queried. + unset( $formats[ array_search( 'standard', $formats, true ) ] ); + } + + // Add any remaining formats to the tax query. + if ( ! empty( $formats ) ) { + // Add the post-format- prefix. + $terms = array_map( + static function ( $format ) { + return 'post-format-' . $format; + }, + $formats + ); + + $tax_query[] = array( + 'taxonomy' => 'post_format', + 'field' => 'slug', + 'terms' => $terms, + 'operator' => 'IN', + ); + } + + // This condition is intended to prevent $tax_query from being added to $query + // if it only contains the relation. + if ( count( $tax_query ) > 1 ) { + $query['tax_query'][] = $tax_query; + } + + return $query; +} +add_filter( 'query_loop_block_query_vars', 'gutenberg_add_format_query_vars_to_query_loop_block', 10, 2 ); diff --git a/lib/compat/wordpress-6.7/class-gutenberg-rest-posts-controller-6-7.php b/lib/compat/wordpress-6.7/class-gutenberg-rest-posts-controller-6-7.php new file mode 100644 index 0000000000000..c7de4371c94f5 --- /dev/null +++ b/lib/compat/wordpress-6.7/class-gutenberg-rest-posts-controller-6-7.php @@ -0,0 +1,698 @@ + 400 ) + ); + } + + // Ensure an include parameter is set in case the orderby is set to 'include'. + if ( ! empty( $request['orderby'] ) && 'include' === $request['orderby'] && empty( $request['include'] ) ) { + return new WP_Error( + 'rest_orderby_include_missing_include', + __( 'You need to define an include parameter to order by include.' ), + array( 'status' => 400 ) + ); + } + + // Retrieve the list of registered collection query parameters. + $registered = $this->get_collection_params(); + $args = array(); + + /* + * This array defines mappings between public API query parameters whose + * values are accepted as-passed, and their internal WP_Query parameter + * name equivalents (some are the same). Only values which are also + * present in $registered will be set. + */ + $parameter_mappings = array( + 'author' => 'author__in', + 'author_exclude' => 'author__not_in', + 'exclude' => 'post__not_in', + 'include' => 'post__in', + 'menu_order' => 'menu_order', + 'offset' => 'offset', + 'order' => 'order', + 'orderby' => 'orderby', + 'page' => 'paged', + 'parent' => 'post_parent__in', + 'parent_exclude' => 'post_parent__not_in', + 'search' => 's', + 'search_columns' => 'search_columns', + 'slug' => 'post_name__in', + 'status' => 'post_status', + ); + + /* + * For each known parameter which is both registered and present in the request, + * set the parameter's value on the query $args. + */ + foreach ( $parameter_mappings as $api_param => $wp_param ) { + if ( isset( $registered[ $api_param ], $request[ $api_param ] ) ) { + $args[ $wp_param ] = $request[ $api_param ]; + } + } + + // Check for & assign any parameters which require special handling or setting. + $args['date_query'] = array(); + + if ( isset( $registered['before'], $request['before'] ) ) { + $args['date_query'][] = array( + 'before' => $request['before'], + 'column' => 'post_date', + ); + } + + if ( isset( $registered['modified_before'], $request['modified_before'] ) ) { + $args['date_query'][] = array( + 'before' => $request['modified_before'], + 'column' => 'post_modified', + ); + } + + if ( isset( $registered['after'], $request['after'] ) ) { + $args['date_query'][] = array( + 'after' => $request['after'], + 'column' => 'post_date', + ); + } + + if ( isset( $registered['modified_after'], $request['modified_after'] ) ) { + $args['date_query'][] = array( + 'after' => $request['modified_after'], + 'column' => 'post_modified', + ); + } + + // Ensure our per_page parameter overrides any provided posts_per_page filter. + if ( isset( $registered['per_page'] ) ) { + $args['posts_per_page'] = $request['per_page']; + } + + if ( isset( $registered['sticky'], $request['sticky'] ) ) { + $sticky_posts = get_option( 'sticky_posts', array() ); + if ( ! is_array( $sticky_posts ) ) { + $sticky_posts = array(); + } + if ( $request['sticky'] ) { + /* + * As post__in will be used to only get sticky posts, + * we have to support the case where post__in was already + * specified. + */ + $args['post__in'] = $args['post__in'] ? array_intersect( $sticky_posts, $args['post__in'] ) : $sticky_posts; + + /* + * If we intersected, but there are no post IDs in common, + * WP_Query won't return "no posts" for post__in = array() + * so we have to fake it a bit. + */ + if ( ! $args['post__in'] ) { + $args['post__in'] = array( 0 ); + } + } elseif ( $sticky_posts ) { + /* + * As post___not_in will be used to only get posts that + * are not sticky, we have to support the case where post__not_in + * was already specified. + */ + $args['post__not_in'] = array_merge( $args['post__not_in'], $sticky_posts ); + } + } + + $args = $this->prepare_tax_query( $args, $request ); + + if ( ! empty( $request['format'] ) ) { + $formats = $request['format']; + $tax_query = array( 'relation' => 'OR' ); + + // The default post format, 'standard', is not stored in the database. + // If 'standard' is part of the request, the query needs to exclude all post items that + // have a format assigned. + if ( in_array( 'standard', $formats, true ) ) { + $tax_query[] = array( + 'taxonomy' => 'post_format', + 'field' => 'slug', + 'terms' => array(), + 'operator' => 'NOT EXISTS', + ); + // Remove the standard format, since it cannot be queried. + unset( $formats[ array_search( 'standard', $formats, true ) ] ); + } + + // Add any remaining formats to the tax query. + if ( ! empty( $formats ) ) { + // Add the post-format- prefix. + $terms = array_map( + static function ( $format ) { + return 'post-format-' . $format; + }, + $formats + ); + + $tax_query[] = array( + 'taxonomy' => 'post_format', + 'field' => 'slug', + 'terms' => $terms, + 'operator' => 'IN', + ); + } + + // Enable filtering by both post formats and other taxonomies by combining them with AND. + if ( isset( $args['tax_query'] ) ) { + $args['tax_query'][] = array( + 'relation' => 'AND', + $tax_query, + ); + } else { + $args['tax_query'] = $tax_query; + } + } + + // Force the post_type argument, since it's not a user input variable. + $args['post_type'] = $this->post_type; + + /** + * Filters WP_Query arguments when querying posts via the REST API. + * + * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug. + * + * Possible hook names include: + * + * - `rest_post_query` + * - `rest_page_query` + * - `rest_attachment_query` + * + * Enables adding extra arguments or setting defaults for a post collection request. + * + * @since 4.7.0 + * @since 5.7.0 Moved after the `tax_query` query arg is generated. + * + * @link https://developer.wordpress.org/reference/classes/wp_query/ + * + * @param array $args Array of arguments for WP_Query. + * @param WP_REST_Request $request The REST API request. + */ + $args = apply_filters( "rest_{$this->post_type}_query", $args, $request ); + $query_args = $this->prepare_items_query( $args, $request ); + + $posts_query = new WP_Query(); + $query_result = $posts_query->query( $query_args ); + + // Allow access to all password protected posts if the context is edit. + if ( 'edit' === $request['context'] ) { + add_filter( 'post_password_required', array( $this, 'check_password_required' ), 10, 2 ); + } + + $posts = array(); + + update_post_author_caches( $query_result ); + update_post_parent_caches( $query_result ); + + if ( post_type_supports( $this->post_type, 'thumbnail' ) ) { + update_post_thumbnail_cache( $posts_query ); + } + + foreach ( $query_result as $post ) { + if ( ! $this->check_read_permission( $post ) ) { + continue; + } + + $data = $this->prepare_item_for_response( $post, $request ); + $posts[] = $this->prepare_response_for_collection( $data ); + } + + // Reset filter. + if ( 'edit' === $request['context'] ) { + remove_filter( 'post_password_required', array( $this, 'check_password_required' ) ); + } + + $page = (int) $query_args['paged']; + $total_posts = $posts_query->found_posts; + + if ( $total_posts < 1 && $page > 1 ) { + // Out-of-bounds, run the query again without LIMIT for total count. + unset( $query_args['paged'] ); + + $count_query = new WP_Query(); + $count_query->query( $query_args ); + $total_posts = $count_query->found_posts; + } + + $max_pages = (int) ceil( $total_posts / (int) $posts_query->query_vars['posts_per_page'] ); + + if ( $page > $max_pages && $total_posts > 0 ) { + return new WP_Error( + 'rest_post_invalid_page_number', + __( 'The page number requested is larger than the number of pages available.' ), + array( 'status' => 400 ) + ); + } + + $response = rest_ensure_response( $posts ); + + $response->header( 'X-WP-Total', (int) $total_posts ); + $response->header( 'X-WP-TotalPages', (int) $max_pages ); + + $request_params = $request->get_query_params(); + $collection_url = rest_url( rest_get_route_for_post_type_items( $this->post_type ) ); + $base = add_query_arg( urlencode_deep( $request_params ), $collection_url ); + + if ( $page > 1 ) { + $prev_page = $page - 1; + + if ( $prev_page > $max_pages ) { + $prev_page = $max_pages; + } + + $prev_link = add_query_arg( 'page', $prev_page, $base ); + $response->link_header( 'prev', $prev_link ); + } + if ( $max_pages > $page ) { + $next_page = $page + 1; + $next_link = add_query_arg( 'page', $next_page, $base ); + + $response->link_header( 'next', $next_link ); + } + + return $response; + } + + /** + * Retrieves the query params for the posts collection. + * + * @since 4.7.0 + * @since 5.4.0 The `tax_relation` query parameter was added. + * @since 5.7.0 The `modified_after` and `modified_before` query parameters were added. + * @since 6.7.0 The `format` query parameter was added. + * + * @return array Collection parameters. + */ + public function get_collection_params() { + $query_params = parent::get_collection_params(); + + $query_params['context']['default'] = 'view'; + + $query_params['after'] = array( + 'description' => __( 'Limit response to posts published after a given ISO8601 compliant date.' ), + 'type' => 'string', + 'format' => 'date-time', + ); + + $query_params['modified_after'] = array( + 'description' => __( 'Limit response to posts modified after a given ISO8601 compliant date.' ), + 'type' => 'string', + 'format' => 'date-time', + ); + + if ( post_type_supports( $this->post_type, 'author' ) ) { + $query_params['author'] = array( + 'description' => __( 'Limit result set to posts assigned to specific authors.' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'default' => array(), + ); + $query_params['author_exclude'] = array( + 'description' => __( 'Ensure result set excludes posts assigned to specific authors.' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'default' => array(), + ); + } + + $query_params['before'] = array( + 'description' => __( 'Limit response to posts published before a given ISO8601 compliant date.' ), + 'type' => 'string', + 'format' => 'date-time', + ); + + $query_params['modified_before'] = array( + 'description' => __( 'Limit response to posts modified before a given ISO8601 compliant date.' ), + 'type' => 'string', + 'format' => 'date-time', + ); + + $query_params['exclude'] = array( + 'description' => __( 'Ensure result set excludes specific IDs.' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'default' => array(), + ); + + $query_params['include'] = array( + 'description' => __( 'Limit result set to specific IDs.' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'default' => array(), + ); + + if ( 'page' === $this->post_type || post_type_supports( $this->post_type, 'page-attributes' ) ) { + $query_params['menu_order'] = array( + 'description' => __( 'Limit result set to posts with a specific menu_order value.' ), + 'type' => 'integer', + ); + } + + $query_params['offset'] = array( + 'description' => __( 'Offset the result set by a specific number of items.' ), + 'type' => 'integer', + ); + + $query_params['order'] = array( + 'description' => __( 'Order sort attribute ascending or descending.' ), + 'type' => 'string', + 'default' => 'desc', + 'enum' => array( 'asc', 'desc' ), + ); + + $query_params['orderby'] = array( + 'description' => __( 'Sort collection by post attribute.' ), + 'type' => 'string', + 'default' => 'date', + 'enum' => array( + 'author', + 'date', + 'id', + 'include', + 'modified', + 'parent', + 'relevance', + 'slug', + 'include_slugs', + 'title', + ), + ); + + if ( 'page' === $this->post_type || post_type_supports( $this->post_type, 'page-attributes' ) ) { + $query_params['orderby']['enum'][] = 'menu_order'; + } + + $post_type = get_post_type_object( $this->post_type ); + + if ( $post_type->hierarchical || 'attachment' === $this->post_type ) { + $query_params['parent'] = array( + 'description' => __( 'Limit result set to items with particular parent IDs.' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'default' => array(), + ); + $query_params['parent_exclude'] = array( + 'description' => __( 'Limit result set to all items except those of a particular parent ID.' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'default' => array(), + ); + } + + $query_params['search_columns'] = array( + 'default' => array(), + 'description' => __( 'Array of column names to be searched.' ), + 'type' => 'array', + 'items' => array( + 'enum' => array( 'post_title', 'post_content', 'post_excerpt' ), + 'type' => 'string', + ), + ); + + $query_params['slug'] = array( + 'description' => __( 'Limit result set to posts with one or more specific slugs.' ), + 'type' => 'array', + 'items' => array( + 'type' => 'string', + ), + ); + + $query_params['status'] = array( + 'default' => 'publish', + 'description' => __( 'Limit result set to posts assigned one or more statuses.' ), + 'type' => 'array', + 'items' => array( + 'enum' => array_merge( array_keys( get_post_stati() ), array( 'any' ) ), + 'type' => 'string', + ), + 'sanitize_callback' => array( $this, 'sanitize_post_statuses' ), + ); + + $query_params = $this->prepare_taxonomy_limit_schema( $query_params ); + + if ( 'post' === $this->post_type ) { + $query_params['sticky'] = array( + 'description' => __( 'Limit result set to items that are sticky.' ), + 'type' => 'boolean', + ); + } + + if ( post_type_supports( $this->post_type, 'post-formats' ) ) { + $query_params['format'] = array( + 'description' => __( 'Limit result set to items assigned one or more given formats.' ), + 'type' => 'array', + 'uniqueItems' => true, + 'items' => array( + 'enum' => array_values( get_post_format_slugs() ), + 'type' => 'string', + ), + ); + } + + /** + * Filters collection parameters for the posts controller. + * + * The dynamic part of the filter `$this->post_type` refers to the post + * type slug for the controller. + * + * This filter registers the collection parameter, but does not map the + * collection parameter to an internal WP_Query parameter. Use the + * `rest_{$this->post_type}_query` filter to set WP_Query parameters. + * + * @since 4.7.0 + * + * @param array $query_params JSON Schema-formatted collection parameters. + * @param WP_Post_Type $post_type Post type object. + */ + return apply_filters( "rest_{$this->post_type}_collection_params", $query_params, $post_type ); + } + + /** + * Prepares the 'tax_query' for a collection of posts. + * + * @since 5.7.0 + * + * @param array $args WP_Query arguments. + * @param WP_REST_Request $request Full details about the request. + * @return array Updated query arguments. + */ + private function prepare_tax_query( array $args, WP_REST_Request $request ) { + $relation = $request['tax_relation']; + + if ( $relation ) { + $args['tax_query'] = array( 'relation' => $relation ); + } + + $taxonomies = wp_list_filter( + get_object_taxonomies( $this->post_type, 'objects' ), + array( 'show_in_rest' => true ) + ); + + foreach ( $taxonomies as $taxonomy ) { + $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name; + + $tax_include = $request[ $base ]; + $tax_exclude = $request[ $base . '_exclude' ]; + + if ( $tax_include ) { + $terms = array(); + $include_children = false; + $operator = 'IN'; + + if ( rest_is_array( $tax_include ) ) { + $terms = $tax_include; + } elseif ( rest_is_object( $tax_include ) ) { + $terms = empty( $tax_include['terms'] ) ? array() : $tax_include['terms']; + $include_children = ! empty( $tax_include['include_children'] ); + + if ( isset( $tax_include['operator'] ) && 'AND' === $tax_include['operator'] ) { + $operator = 'AND'; + } + } + + if ( $terms ) { + $args['tax_query'][] = array( + 'taxonomy' => $taxonomy->name, + 'field' => 'term_id', + 'terms' => $terms, + 'include_children' => $include_children, + 'operator' => $operator, + ); + } + } + + if ( $tax_exclude ) { + $terms = array(); + $include_children = false; + + if ( rest_is_array( $tax_exclude ) ) { + $terms = $tax_exclude; + } elseif ( rest_is_object( $tax_exclude ) ) { + $terms = empty( $tax_exclude['terms'] ) ? array() : $tax_exclude['terms']; + $include_children = ! empty( $tax_exclude['include_children'] ); + } + + if ( $terms ) { + $args['tax_query'][] = array( + 'taxonomy' => $taxonomy->name, + 'field' => 'term_id', + 'terms' => $terms, + 'include_children' => $include_children, + 'operator' => 'NOT IN', + ); + } + } + } + + return $args; + } + + /** + * Prepares the collection schema for including and excluding items by terms. + * + * @since 5.7.0 + * + * @param array $query_params Collection schema. + * @return array Updated schema. + */ + private function prepare_taxonomy_limit_schema( array $query_params ) { + $taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) ); + + if ( ! $taxonomies ) { + return $query_params; + } + + $query_params['tax_relation'] = array( + 'description' => __( 'Limit result set based on relationship between multiple taxonomies.' ), + 'type' => 'string', + 'enum' => array( 'AND', 'OR' ), + ); + + $limit_schema = array( + 'type' => array( 'object', 'array' ), + 'oneOf' => array( + array( + 'title' => __( 'Term ID List' ), + 'description' => __( 'Match terms with the listed IDs.' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + ), + array( + 'title' => __( 'Term ID Taxonomy Query' ), + 'description' => __( 'Perform an advanced term query.' ), + 'type' => 'object', + 'properties' => array( + 'terms' => array( + 'description' => __( 'Term IDs.' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'default' => array(), + ), + 'include_children' => array( + 'description' => __( 'Whether to include child terms in the terms limiting the result set.' ), + 'type' => 'boolean', + 'default' => false, + ), + ), + 'additionalProperties' => false, + ), + ), + ); + + $include_schema = array_merge( + array( + /* translators: %s: Taxonomy name. */ + 'description' => __( 'Limit result set to items with specific terms assigned in the %s taxonomy.' ), + ), + $limit_schema + ); + // 'operator' is supported only for 'include' queries. + $include_schema['oneOf'][1]['properties']['operator'] = array( + 'description' => __( 'Whether items must be assigned all or any of the specified terms.' ), + 'type' => 'string', + 'enum' => array( 'AND', 'OR' ), + 'default' => 'OR', + ); + + $exclude_schema = array_merge( + array( + /* translators: %s: Taxonomy name. */ + 'description' => __( 'Limit result set to items except those with specific terms assigned in the %s taxonomy.' ), + ), + $limit_schema + ); + + foreach ( $taxonomies as $taxonomy ) { + $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name; + $base_exclude = $base . '_exclude'; + + $query_params[ $base ] = $include_schema; + $query_params[ $base ]['description'] = sprintf( $query_params[ $base ]['description'], $base ); + + $query_params[ $base_exclude ] = $exclude_schema; + $query_params[ $base_exclude ]['description'] = sprintf( $query_params[ $base_exclude ]['description'], $base ); + + if ( ! $taxonomy->hierarchical ) { + unset( $query_params[ $base ]['oneOf'][1]['properties']['include_children'] ); + unset( $query_params[ $base_exclude ]['oneOf'][1]['properties']['include_children'] ); + } + } + + return $query_params; + } +} diff --git a/lib/compat/wordpress-6.7/post-formats.php b/lib/compat/wordpress-6.7/post-formats.php new file mode 100644 index 0000000000000..d3de5b83957e2 --- /dev/null +++ b/lib/compat/wordpress-6.7/post-formats.php @@ -0,0 +1,24 @@ + { + const normalizedA = a.label.toUpperCase(); + const normalizedB = b.label.toUpperCase(); + + if ( normalizedA < normalizedB ) { + return -1; + } + if ( normalizedA > normalizedB ) { + return 1; + } + return 0; +} ); + +// A helper function to convert translatable post format names into their static values. +function formatNamesToValues( names, formats ) { + return names + .map( ( name ) => { + return formats.find( + ( item ) => + item.label.toLocaleLowerCase() === name.toLocaleLowerCase() + )?.value; + } ) + .filter( Boolean ); +} + +export default function FormatControls( { onChange, query: { format } } ) { + // 'format' is expected to be an array. If it is not an array, for example + // if a user has manually entered an invalid value in the block markup, + // convert it to an array to prevent JavaScript errors. + const normalizedFormats = Array.isArray( format ) ? format : [ format ]; + + const { supportedFormats } = useSelect( ( select ) => { + const themeSupports = select( coreStore ).getThemeSupports(); + return { + supportedFormats: themeSupports.formats, + }; + }, [] ); + + const formats = POST_FORMATS.filter( ( item ) => + supportedFormats.includes( item.value ) + ); + + const values = normalizedFormats + .map( + ( name ) => formats.find( ( item ) => item.value === name )?.label + ) + .filter( Boolean ); + + const suggestions = formats + .filter( ( item ) => ! format.includes( item.value ) ) + .map( ( item ) => item.label ); + + return ( + { + onChange( { + format: formatNamesToValues( newValues, formats ), + } ); + } } + __experimentalShowHowTo={ false } + __experimentalExpandOnFocus + __nextHasNoMarginBottom + __next40pxDefaultSize + /> + ); +} diff --git a/packages/block-library/src/query/edit/inspector-controls/index.js b/packages/block-library/src/query/edit/inspector-controls/index.js index 010e57b2da4fc..6c246ab89b3b3 100644 --- a/packages/block-library/src/query/edit/inspector-controls/index.js +++ b/packages/block-library/src/query/edit/inspector-controls/index.js @@ -12,6 +12,8 @@ import { __experimentalToolsPanel as ToolsPanel, __experimentalToolsPanelItem as ToolsPanelItem, } from '@wordpress/components'; +import { useSelect } from '@wordpress/data'; +import { store as coreStore } from '@wordpress/core-data'; import { __ } from '@wordpress/i18n'; import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; import { debounce } from '@wordpress/compose'; @@ -24,6 +26,7 @@ import OrderControl from './order-control'; import AuthorControl from './author-control'; import ParentControl from './parent-control'; import { TaxonomyControls } from './taxonomy-controls'; +import FormatControls from './format-controls'; import StickyControl from './sticky-control'; import CreateNewPostLink from './create-new-post-link'; import PerPageControl from './per-page-control'; @@ -56,10 +59,15 @@ export default function QueryInspectorControls( props ) { inherit, taxQuery, parents, + format, } = query; const allowedControls = useAllowedControls( attributes ); const [ showSticky, setShowSticky ] = useState( postType === 'post' ); - const { postTypesTaxonomiesMap, postTypesSelectOptions } = usePostTypes(); + const { + postTypesTaxonomiesMap, + postTypesSelectOptions, + postTypeFormatSupportMap, + } = usePostTypes(); const taxonomies = useTaxonomies( postType ); const isPostTypeHierarchical = useIsPostTypeHierarchical( postType ); useEffect( () => { @@ -88,6 +96,13 @@ export default function QueryInspectorControls( props ) { } // We need to reset `parents` because they are tied to each post type. updateQuery.parents = []; + // Post types can register post format support with `add_post_type_support`. + // But we need to reset the `format` property when switching to post types + // that do not support post formats. + const hasFormatSupport = postTypeFormatSupportMap[ newValue ]; + if ( ! hasFormatSupport ) { + updateQuery.format = []; + } setQuery( updateQuery ); }; const [ querySearch, setQuerySearch ] = useState( query.search ); @@ -132,11 +147,36 @@ export default function QueryInspectorControls( props ) { isControlAllowed( allowedControls, 'parents' ) && isPostTypeHierarchical; + const postTypeHasFormatSupport = postTypeFormatSupportMap[ postType ]; + const showFormatControl = useSelect( + ( select ) => { + // Check if the post type supports post formats and if the control is allowed. + if ( + ! postTypeHasFormatSupport || + ! isControlAllowed( allowedControls, 'format' ) + ) { + return false; + } + + const themeSupports = select( coreStore ).getThemeSupports(); + + // If there are no supported formats, getThemeSupports still includes the default 'standard' format, + // and in this case the control should not be shown since the user has no other formats to choose from. + return ( + themeSupports.formats && + themeSupports.formats.length > 0 && + themeSupports.formats.some( ( type ) => type !== 'standard' ) + ); + }, + [ allowedControls, postTypeHasFormatSupport ] + ); + const showFiltersPanel = showTaxControl || showAuthorControl || showSearchControl || - showParentControl; + showParentControl || + showFormatControl; const dropdownMenuProps = useToolsPanelDropdownMenuProps(); const showPostCountControl = isControlAllowed( @@ -313,6 +353,7 @@ export default function QueryInspectorControls( props ) { parents: [], search: '', taxQuery: null, + format: [], } ); setQuerySearch( '' ); } } @@ -374,6 +415,18 @@ export default function QueryInspectorControls( props ) { /> ) } + { showFormatControl && ( + !! format?.length } + label={ __( 'Formats' ) } + onDeselect={ () => setQuery( { format: [] } ) } + > + + + ) } ) } diff --git a/packages/block-library/src/query/utils.js b/packages/block-library/src/query/utils.js index 2e9412b1683cb..68da2573bab0f 100644 --- a/packages/block-library/src/query/utils.js +++ b/packages/block-library/src/query/utils.js @@ -94,6 +94,7 @@ export const mapToIHasNameAndId = ( entities, path ) => { * Returns a helper object that contains: * 1. An `options` object from the available post types, to be passed to a `SelectControl`. * 2. A helper map with available taxonomies per post type. + * 3. A helper map with post format support per post type. * * @return {Object} The helper object related to post types. */ @@ -124,7 +125,21 @@ export const usePostTypes = () => { } ) ), [ postTypes ] ); - return { postTypesTaxonomiesMap, postTypesSelectOptions }; + const postTypeFormatSupportMap = useMemo( () => { + if ( ! postTypes?.length ) { + return {}; + } + return postTypes.reduce( ( accumulator, type ) => { + accumulator[ type.slug ] = + type.supports?.[ 'post-formats' ] || false; + return accumulator; + }, {} ); + }, [ postTypes ] ); + return { + postTypesTaxonomiesMap, + postTypesSelectOptions, + postTypeFormatSupportMap, + }; }; /** diff --git a/test/integration/fixtures/blocks/core__query.json b/test/integration/fixtures/blocks/core__query.json index b050aaa2b5b1f..a5ee4523128df 100644 --- a/test/integration/fixtures/blocks/core__query.json +++ b/test/integration/fixtures/blocks/core__query.json @@ -16,7 +16,8 @@ "sticky": "", "inherit": true, "taxQuery": null, - "parents": [] + "parents": [], + "format": [] }, "tagName": "div", "enhancedPagination": false From ea0ea782b827cb0a9f1e4a3672394ae7f95bb411 Mon Sep 17 00:00:00 2001 From: Jerry Jones Date: Mon, 16 Sep 2024 02:03:54 -0500 Subject: [PATCH 13/17] Put Replace in own group to give it borders (#64849) Co-authored-by: jeryj Co-authored-by: richtabor Co-authored-by: MaggieCabrera --- packages/block-library/src/image/image.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/block-library/src/image/image.js b/packages/block-library/src/image/image.js index 60d83f8912907..1673d36e463d5 100644 --- a/packages/block-library/src/image/image.js +++ b/packages/block-library/src/image/image.js @@ -559,7 +559,8 @@ export default function Image( { const mediaReplaceFlow = isSingleSelected && ! isEditingImage && ! lockUrlControls && ( - + // For contentOnly mode, put this button in its own area so it has borders around it. + Date: Mon, 16 Sep 2024 09:27:24 +0200 Subject: [PATCH 14/17] Pass the comments query paged arg to functions (#63698) Co-authored-by: SantosGuillamot Co-authored-by: ockham Co-authored-by: gziolo --- packages/block-library/src/comments-pagination-next/index.php | 2 +- .../block-library/src/comments-pagination-previous/index.php | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/block-library/src/comments-pagination-next/index.php b/packages/block-library/src/comments-pagination-next/index.php index cb8350e561b6e..14a323df3a13a 100644 --- a/packages/block-library/src/comments-pagination-next/index.php +++ b/packages/block-library/src/comments-pagination-next/index.php @@ -37,7 +37,7 @@ function render_block_core_comments_pagination_next( $attributes, $content, $blo $label .= $pagination_arrow; } - $next_comments_link = get_next_comments_link( $label, $max_page ); + $next_comments_link = get_next_comments_link( $label, $max_page, $comment_vars['paged'] ); remove_filter( 'next_posts_link_attributes', $filter_link_attributes ); diff --git a/packages/block-library/src/comments-pagination-previous/index.php b/packages/block-library/src/comments-pagination-previous/index.php index 092a28da67792..b70a9f609da9f 100644 --- a/packages/block-library/src/comments-pagination-previous/index.php +++ b/packages/block-library/src/comments-pagination-previous/index.php @@ -29,7 +29,8 @@ function render_block_core_comments_pagination_previous( $attributes, $content, }; add_filter( 'previous_comments_link_attributes', $filter_link_attributes ); - $previous_comments_link = get_previous_comments_link( $label ); + $comment_vars = build_comment_query_vars_from_block( $block ); + $previous_comments_link = get_previous_comments_link( $label, $comment_vars['paged'] ); remove_filter( 'previous_comments_link_attributes', $filter_link_attributes ); From 38e74db14c27f4ae075eebcf12ee846fcd20fd8b Mon Sep 17 00:00:00 2001 From: Damon Cook Date: Mon, 16 Sep 2024 04:33:38 -0400 Subject: [PATCH 15/17] Add JSDoc block for getSectionRootClientId in block editor package (#65219) * Add JSDoc block for getSectionRootClientId in block editor * Apply suggestions from code review Co-authored-by: Dave Smith --------- Co-authored-by: Dave Smith --- packages/block-editor/src/store/private-selectors.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/block-editor/src/store/private-selectors.js b/packages/block-editor/src/store/private-selectors.js index d76f90bc94ffe..01ad8f69febc9 100644 --- a/packages/block-editor/src/store/private-selectors.js +++ b/packages/block-editor/src/store/private-selectors.js @@ -548,6 +548,15 @@ export function isZoomOutMode( state ) { return state.editorMode === 'zoom-out'; } +/** + * Retrieves the client ID of the block which contains the blocks + * acting as "sections" in the editor. This is typically the "main content" + * of the template/post. + * + * @param {Object} state Editor state. + * + * @return {string|undefined} The section root client ID or undefined if not set. + */ export function getSectionRootClientId( state ) { return state.settings?.[ sectionRootClientIdKey ]; } From 4ca78cdf8ec71ba62dbf4403662202872d8e5888 Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Mon, 16 Sep 2024 19:35:24 +0900 Subject: [PATCH 16/17] Fix Tabs styling in Font Library modal (#65330) Co-authored-by: mirka <0mirka00@git.wordpress.org> Co-authored-by: DaniGuardiola Co-authored-by: ciampo --- .../global-styles/font-library-modal/index.js | 52 +++++++++---------- .../font-library-modal/style.scss | 19 ++++--- 2 files changed, 35 insertions(+), 36 deletions(-) diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/index.js b/packages/edit-site/src/components/global-styles/font-library-modal/index.js index 5af4be90fecdc..495652f144275 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/index.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/index.js @@ -66,8 +66,8 @@ function FontLibraryModal( { isFullScreen className="font-library-modal" > -
- + +
{ tabs.map( ( { id, title } ) => ( @@ -75,30 +75,30 @@ function FontLibraryModal( { ) ) } - { tabs.map( ( { id } ) => { - let contents; - switch ( id ) { - case 'upload-fonts': - contents = ; - break; - case 'installed-fonts': - contents = ; - break; - default: - contents = ; - } - return ( - - { contents } - - ); - } ) } - -
+
+ { tabs.map( ( { id } ) => { + let contents; + switch ( id ) { + case 'upload-fonts': + contents = ; + break; + case 'installed-fonts': + contents = ; + break; + default: + contents = ; + } + return ( + + { contents } + + ); + } ) } + ); } diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/style.scss b/packages/edit-site/src/components/global-styles/font-library-modal/style.scss index a9a0784cbcb5a..8d6f8a3c9b0af 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/style.scss +++ b/packages/edit-site/src/components/global-styles/font-library-modal/style.scss @@ -129,18 +129,17 @@ $footer-height: 70px; padding-bottom: $grid-unit-20; } -.font-library-modal__tabs { - [role="tablist"] { - position: sticky; - top: 0; - border-bottom: 1px solid $gray-300; - background: $white; - margin: 0 #{$grid-unit-40 * -1}; - padding: 0 $grid-unit-20; - z-index: 1; - } +.font-library-modal__tablist { + position: sticky; + top: 0; + border-bottom: 1px solid $gray-300; + background: $white; + margin: 0 #{$grid-unit-40 * -1}; + padding: 0 $grid-unit-20; + z-index: 1; } + .font-library-modal__upload-area { align-items: center; display: flex; From c80fc61804e99268eec4441e2915b33368c30b18 Mon Sep 17 00:00:00 2001 From: Hit Bhalodia <58802366+hbhalodia@users.noreply.github.com> Date: Mon, 16 Sep 2024 17:38:43 +0530 Subject: [PATCH 17/17] Fix: Button: Replace remaining 40px default size violation [Edit Site 2] (#65258) * Fix font library modal fonts to use 40px default button size * Fix install font button to use default 40px size * Fix the installed fonts button size to use 40px default * Fix upload font button size to use 40px default size * Fix add the default button 40px size for randomize color pallete button * Fix edit site style revision button to use 40px default size * Fix edit site shadow components buttons to use default 40px size * Fix the style for the button height on the font library modal * Add !important to override the button size for 40px Co-authored-by: hbhalodia Co-authored-by: mirka <0mirka00@git.wordpress.org> --- .../global-styles/font-library-modal/font-card.js | 3 +-- .../font-library-modal/font-collection.js | 3 +-- .../font-library-modal/installed-fonts.js | 6 ++---- .../global-styles/font-library-modal/style.scss | 12 ++++++++++-- .../global-styles/font-library-modal/upload-fonts.js | 3 +-- .../src/components/global-styles/palette.js | 3 +-- .../screen-revisions/revisions-buttons.js | 3 +-- .../components/global-styles/shadows-edit-panel.js | 12 ++++-------- 8 files changed, 21 insertions(+), 24 deletions(-) diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/font-card.js b/packages/edit-site/src/components/global-styles/font-library-modal/font-card.js index 735179588d072..579c6564fdf3e 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/font-card.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/font-card.js @@ -28,8 +28,7 @@ function FontCard( { font, onClick, variantsText, navigatorPath } ) { return (