From 89b95e1d21def2a172199ed5df70879aeab7b97a Mon Sep 17 00:00:00 2001 From: Jarda Snajdr Date: Tue, 23 Sep 2025 14:06:24 +0200 Subject: [PATCH 01/58] Fix creating templates for posts with long slugs (#71838) --- .../add-custom-template-modal-content.js | 3 +- .../src/components/add-new-template/utils.js | 30 +++++++++++++++++-- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/packages/edit-site/src/components/add-new-template/add-custom-template-modal-content.js b/packages/edit-site/src/components/add-new-template/add-custom-template-modal-content.js index 4929ace507af8e..03f3223e4576d2 100644 --- a/packages/edit-site/src/components/add-new-template/add-custom-template-modal-content.js +++ b/packages/edit-site/src/components/add-new-template/add-custom-template-modal-content.js @@ -17,6 +17,7 @@ import { useEntityRecords } from '@wordpress/core-data'; import { decodeEntities } from '@wordpress/html-entities'; import { useDebouncedInput } from '@wordpress/compose'; import { focus } from '@wordpress/dom'; +import { safeDecodeURI } from '@wordpress/url'; /** * Internal dependencies @@ -67,7 +68,7 @@ function SuggestionListItem( { lineHeight={ 1.53846153846 } // 20px className={ `${ baseCssClass }__info` } > - { suggestion.link } + { safeDecodeURI( suggestion.link ) } ) } diff --git a/packages/edit-site/src/components/add-new-template/utils.js b/packages/edit-site/src/components/add-new-template/utils.js index c7bd9ecb11729b..b471c7b754df47 100644 --- a/packages/edit-site/src/components/add-new-template/utils.js +++ b/packages/edit-site/src/components/add-new-template/utils.js @@ -7,6 +7,7 @@ import { decodeEntities } from '@wordpress/html-entities'; import { useMemo, useCallback } from '@wordpress/element'; import { __, _x, sprintf } from '@wordpress/i18n'; import { blockMeta, post, archive } from '@wordpress/icons'; +import { safeDecodeURI } from '@wordpress/url'; /** * Internal dependencies @@ -29,6 +30,20 @@ const getValueFromObjectPath = ( object, path ) => { return value; }; +/** + * Helper that adds a prefix to a post slug. The slug needs to be URL-decoded first, + * so that we have raw Unicode characters there. The server will truncate the slug to + * 200 characters, respecing Unicode char boundary. On the other hand, the server + * doesn't detect urlencoded octet boundary and can possibly construct slugs that + * are not valid urlencoded strings. + * @param {string} prefix The prefix to add to the slug. + * @param {string} slug The slug to add the prefix to. + * @return {string} The slug with the prefix. + */ +function prefixSlug( prefix, slug ) { + return `${ prefix }-${ safeDecodeURI( slug ) }`; +} + /** * Helper util to map records to add a `name` prop from a * provided path, in order to handle all entities in the same @@ -306,7 +321,10 @@ export const usePostTypeMenuItems = ( onClickMenuItem ) => { }; }, getSpecificTemplate: ( suggestion ) => { - const templateSlug = `${ templatePrefixes[ slug ] }-${ suggestion.slug }`; + const templateSlug = prefixSlug( + templatePrefixes[ slug ], + suggestion.slug + ); return { title: templateSlug, slug: templateSlug, @@ -460,7 +478,10 @@ export const useTaxonomiesMenuItems = ( onClickMenuItem ) => { }; }, getSpecificTemplate: ( suggestion ) => { - const templateSlug = `${ templatePrefixes[ slug ] }-${ suggestion.slug }`; + const templateSlug = prefixSlug( + templatePrefixes[ slug ], + suggestion.slug + ); return { title: templateSlug, slug: templateSlug, @@ -545,7 +566,10 @@ export function useAuthorMenuItem( onClickMenuItem ) { }; }, getSpecificTemplate: ( suggestion ) => { - const templateSlug = `author-${ suggestion.slug }`; + const templateSlug = prefixSlug( + 'author', + suggestion.slug + ); return { title: sprintf( // translators: %s: Name of the author e.g: "Admin". From 5a096cc31bc0c6b8c95077eadd7418b4999898d1 Mon Sep 17 00:00:00 2001 From: Ben Dwyer Date: Tue, 23 Sep 2025 13:41:05 +0100 Subject: [PATCH 02/58] Time To Read: Make display as range the default, and allow older blocks to migrate to this setting (#71842) * Time To Read: Make display as range the default, and allow older blocks to migrate to this setting * update fixtures --- .../src/post-time-to-read/block.json | 2 +- .../src/post-time-to-read/edit.js | 39 +------------------ .../blocks/core__post-time-to-read.json | 2 +- 3 files changed, 4 insertions(+), 39 deletions(-) diff --git a/packages/block-library/src/post-time-to-read/block.json b/packages/block-library/src/post-time-to-read/block.json index 85058098456be2..cc360d70701b1b 100644 --- a/packages/block-library/src/post-time-to-read/block.json +++ b/packages/block-library/src/post-time-to-read/block.json @@ -14,7 +14,7 @@ }, "displayAsRange": { "type": "boolean", - "default": false + "default": true }, "averageReadingSpeed": { "type": "number", diff --git a/packages/block-library/src/post-time-to-read/edit.js b/packages/block-library/src/post-time-to-read/edit.js index fb56fb62c2914f..cdd125407c9f35 100644 --- a/packages/block-library/src/post-time-to-read/edit.js +++ b/packages/block-library/src/post-time-to-read/edit.js @@ -7,13 +7,12 @@ import clsx from 'clsx'; * WordPress dependencies */ import { __, _x, _n, sprintf } from '@wordpress/i18n'; -import { useMemo, useEffect } from '@wordpress/element'; +import { useMemo } from '@wordpress/element'; import { AlignmentControl, BlockControls, InspectorControls, useBlockProps, - store as blockEditorStore, } from '@wordpress/block-editor'; import { ToggleControl, @@ -22,7 +21,6 @@ import { } from '@wordpress/components'; import { __unstableSerializeAndClean } from '@wordpress/blocks'; import { useEntityProp, useEntityBlockEditor } from '@wordpress/core-data'; -import { useDispatch, useSelect } from '@wordpress/data'; import { count as wordCount } from '@wordpress/wordcount'; /** @@ -30,41 +28,8 @@ import { count as wordCount } from '@wordpress/wordcount'; */ import { useToolsPanelDropdownMenuProps } from '../utils/hooks'; -function PostTimeToReadEdit( { - attributes, - setAttributes, - clientId, - context, -} ) { +function PostTimeToReadEdit( { attributes, setAttributes, context } ) { const { textAlign, displayAsRange, averageReadingSpeed } = attributes; - const { __unstableMarkNextChangeAsNotPersistent } = - useDispatch( blockEditorStore ); - - const { blockWasJustInserted } = useSelect( - ( select ) => { - const { wasBlockJustInserted } = select( blockEditorStore ); - - return { - blockWasJustInserted: wasBlockJustInserted( clientId ), - }; - }, - [ clientId ] - ); - - // When the block is first inserted, default to displaying as a range. - useEffect( () => { - if ( blockWasJustInserted ) { - __unstableMarkNextChangeAsNotPersistent(); - setAttributes( { - displayAsRange: true, - } ); - } - }, [ - blockWasJustInserted, - __unstableMarkNextChangeAsNotPersistent, - setAttributes, - ] ); - const { postId, postType } = context; const dropdownMenuProps = useToolsPanelDropdownMenuProps(); diff --git a/test/integration/fixtures/blocks/core__post-time-to-read.json b/test/integration/fixtures/blocks/core__post-time-to-read.json index 6a7a394597209b..a3c5a983f19ae0 100644 --- a/test/integration/fixtures/blocks/core__post-time-to-read.json +++ b/test/integration/fixtures/blocks/core__post-time-to-read.json @@ -3,7 +3,7 @@ "name": "core/post-time-to-read", "isValid": true, "attributes": { - "displayAsRange": false, + "displayAsRange": true, "averageReadingSpeed": 189 }, "innerBlocks": [] From 5ca68bc5090465c12cccdff4a2451aa1849d8909 Mon Sep 17 00:00:00 2001 From: Roberto Aranda Date: Tue, 23 Sep 2025 20:58:25 +0200 Subject: [PATCH 03/58] Button: Fix incorrect padding with text and right icon (#71464) * Button: Fix incorrect padding with text and right icon * Update snapshots * Add CHANGELOG note * Move changelog note under "Unreleased" --------- Co-authored-by: Andrew Duthie Co-authored-by: Andrew Duthie <1779930+aduth@users.noreply.github.com> --- packages/components/CHANGELOG.md | 4 ++++ packages/components/src/button/index.tsx | 1 + packages/components/src/button/style.scss | 4 ++++ .../post-publish-panel/test/__snapshots__/index.js.snap | 4 ++-- 4 files changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 5ebdea267d252f..29aa6f4bf78f28 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -6,6 +6,10 @@ - `TextareaControl`: Add default resize: vertical rule ([#71736](https://github.com/WordPress/gutenberg/pull/71736)). +### Bug Fixes + +- `Button`: Fix incorrect padding with text and right icon ([#71464](https://github.com/WordPress/gutenberg/pull/71464)). + ### Internal - Expose `ValidatedFormTokenField` via private APIs [#71194](https://github.com/WordPress/gutenberg/pull/71194) diff --git a/packages/components/src/button/index.tsx b/packages/components/src/button/index.tsx index 0ab6b24b4dd6aa..ca4533c679108b 100644 --- a/packages/components/src/button/index.tsx +++ b/packages/components/src/button/index.tsx @@ -161,6 +161,7 @@ export function UnforwardedButton( 'is-destructive': isDestructive, 'has-text': !! icon && ( hasChildren || text ), 'has-icon': !! icon, + 'has-icon-right': iconPosition === 'right', } ); const trulyDisabled = disabled && ! accessibleWhenDisabled; diff --git a/packages/components/src/button/style.scss b/packages/components/src/button/style.scss index 2d993c5942cc89..5d734cc17442e7 100644 --- a/packages/components/src/button/style.scss +++ b/packages/components/src/button/style.scss @@ -342,6 +342,10 @@ padding-right: $grid-unit-15; padding-left: $grid-unit-10; gap: $grid-unit-05; + &.has-icon-right { + padding-right: $grid-unit-10; + padding-left: $grid-unit-15; + } } } diff --git a/packages/editor/src/components/post-publish-panel/test/__snapshots__/index.js.snap b/packages/editor/src/components/post-publish-panel/test/__snapshots__/index.js.snap index 5f04209a079f12..a69ffc3550389e 100644 --- a/packages/editor/src/components/post-publish-panel/test/__snapshots__/index.js.snap +++ b/packages/editor/src/components/post-publish-panel/test/__snapshots__/index.js.snap @@ -156,7 +156,7 @@ exports[`PostPublishPanel should render the post-publish panel if the post is pu class="post-publish-panel__postpublish-buttons" > @@ -383,7 +383,7 @@ exports[`PostPublishPanel should render the post-publish panel if the post is sc class="post-publish-panel__postpublish-buttons" > From 942492a4607358642c05b145ee346f228f87727a Mon Sep 17 00:00:00 2001 From: Aki Hamano <54422211+t-hamano@users.noreply.github.com> Date: Wed, 24 Sep 2025 10:23:56 +0900 Subject: [PATCH 04/58] Accordion Blocks: Standardize CSS class names (#71785) Co-authored-by: t-hamano Co-authored-by: mikachan --- .../src/accordion-content/block.json | 3 +- .../src/accordion-content/index.php | 2 +- .../src/accordion-content/save.js | 14 +-- .../src/accordion-content/style.scss | 21 +++++ .../src/accordion-header/block.json | 2 +- .../src/accordion-header/edit.js | 6 +- .../src/accordion-header/save.js | 8 +- .../src/accordion-header/style.scss | 39 ++++++++ .../src/accordion-panel/block.json | 3 +- .../src/accordion-panel/style.scss | 9 ++ .../block-library/src/accordion/block.json | 1 - .../block-library/src/accordion/style.scss | 90 ------------------- packages/block-library/src/style.scss | 4 +- 13 files changed, 89 insertions(+), 113 deletions(-) create mode 100644 packages/block-library/src/accordion-content/style.scss create mode 100644 packages/block-library/src/accordion-header/style.scss create mode 100644 packages/block-library/src/accordion-panel/style.scss delete mode 100644 packages/block-library/src/accordion/style.scss diff --git a/packages/block-library/src/accordion-content/block.json b/packages/block-library/src/accordion-content/block.json index c7271259a4e431..47d238accfab1c 100644 --- a/packages/block-library/src/accordion-content/block.json +++ b/packages/block-library/src/accordion-content/block.json @@ -52,5 +52,6 @@ "default": false } }, - "textdomain": "default" + "textdomain": "default", + "style": "wp-block-accordion-content" } diff --git a/packages/block-library/src/accordion-content/index.php b/packages/block-library/src/accordion-content/index.php index e459b2ac38aa75..ce24b0e71bed82 100644 --- a/packages/block-library/src/accordion-content/index.php +++ b/packages/block-library/src/accordion-content/index.php @@ -37,7 +37,7 @@ function block_core_accordion_content_render( $attributes, $content ) { $p->set_attribute( 'data-wp-class--is-open', 'state.isOpen' ); $p->set_attribute( 'data-wp-init', 'callbacks.initAccordionContents' ); - if ( $p->next_tag( array( 'class_name' => 'accordion-content__toggle' ) ) ) { + if ( $p->next_tag( array( 'class_name' => 'wp-block-accordion-header__toggle' ) ) ) { $p->set_attribute( 'data-wp-on--click', 'actions.toggle' ); $p->set_attribute( 'data-wp-on--keydown', 'actions.handleKeyDown' ); $p->set_attribute( 'id', $unique_id ); diff --git a/packages/block-library/src/accordion-content/save.js b/packages/block-library/src/accordion-content/save.js index 04d95466593c70..6334f47878cca0 100644 --- a/packages/block-library/src/accordion-content/save.js +++ b/packages/block-library/src/accordion-content/save.js @@ -9,17 +9,11 @@ import clsx from 'clsx'; export default function save( { attributes } ) { const { openByDefault } = attributes; - const blockProps = useBlockProps.save(); - const className = clsx( - { + const blockProps = useBlockProps.save( { + className: clsx( { 'is-open': openByDefault, - }, - blockProps.className - ); - const innerBlocksProps = useInnerBlocksProps.save( { - ...blockProps, - className, + } ), } ); - + const innerBlocksProps = useInnerBlocksProps.save( blockProps ); return
; } diff --git a/packages/block-library/src/accordion-content/style.scss b/packages/block-library/src/accordion-content/style.scss new file mode 100644 index 00000000000000..abff7249222f2d --- /dev/null +++ b/packages/block-library/src/accordion-content/style.scss @@ -0,0 +1,21 @@ +.wp-block-accordion-content { + display: grid; + grid-template-rows: max-content 0fr; + + &.is-open { + grid-template-rows: max-content 1fr; + + > .wp-block-accordion-header .wp-block-accordion-header__toggle-icon { + transform: rotate(45deg); + } + } + + /* Add transitions only for users who do not prefer reduced motion */ + @media (prefers-reduced-motion: no-preference) { + transition: grid-template-rows 0.3s ease-out; + + > .wp-block-accordion-header .wp-block-accordion-header__toggle-icon { + transition: transform 0.2s ease-in-out; + } + } +} diff --git a/packages/block-library/src/accordion-header/block.json b/packages/block-library/src/accordion-header/block.json index 5f3372a00982fb..c70d8e29ca890e 100644 --- a/packages/block-library/src/accordion-header/block.json +++ b/packages/block-library/src/accordion-header/block.json @@ -62,7 +62,7 @@ "title": { "type": "rich-text", "source": "rich-text", - "selector": ".accordion-content__toggle-title", + "selector": ".wp-block-accordion-header__toggle-title", "role": "content" }, "level": { diff --git a/packages/block-library/src/accordion-header/edit.js b/packages/block-library/src/accordion-header/edit.js index 6e1fa380f257dd..27e2de8f542b31 100644 --- a/packages/block-library/src/accordion-header/edit.js +++ b/packages/block-library/src/accordion-header/edit.js @@ -56,14 +56,14 @@ export default function Edit( { attributes, setAttributes, context } ) { - ) } - - { isFocused && - thread.reply.map( ( reply ) => ( - - { 'approved' !== thread.status && ( - - ) } - { 'approved' === thread.status && ( - - ) } - - ) ) } - + { isFocused && + replies.map( ( reply ) => ( + + + + ) ) } + { ! isFocused && restReplies.length > 0 && ( + + + + ) } + { ! isFocused && lastReply && ( + ) } { isFocused && ( Date: Thu, 25 Sep 2025 13:03:21 +0100 Subject: [PATCH 30/58] Accordion Header: Remove textAlignment and textAlign (#71875) * Remove textAlignment attribute * Remove textAlign logic * Remove clsx * Remove unused classname * Remove textAlign support Co-authored-by: mikachan Co-authored-by: t-hamano --- docs/reference-guides/core-blocks.md | 4 ++-- .../block-library/src/accordion-header/block.json | 5 ----- .../block-library/src/accordion-header/edit.js | 14 +++----------- .../block-library/src/accordion-header/save.js | 14 +++----------- 4 files changed, 8 insertions(+), 29 deletions(-) diff --git a/docs/reference-guides/core-blocks.md b/docs/reference-guides/core-blocks.md index 003883ab022dc4..9567004553a93b 100644 --- a/docs/reference-guides/core-blocks.md +++ b/docs/reference-guides/core-blocks.md @@ -39,8 +39,8 @@ Displays an accordion header. ([Source](https://github.com/WordPress/gutenberg/t - **Experimental:** true - **Category:** design - **Parent:** core/accordion-content -- **Supports:** anchor, color (background, gradients, text), interactivity, shadow, spacing (padding), typography (fontSize, textAlign), ~~align~~ -- **Attributes:** iconPosition, level, levelOptions, openByDefault, showIcon, textAlignment, title +- **Supports:** anchor, color (background, gradients, text), interactivity, shadow, spacing (padding), typography (fontSize), ~~align~~ +- **Attributes:** iconPosition, level, levelOptions, openByDefault, showIcon, title ## Accordion Panel diff --git a/packages/block-library/src/accordion-header/block.json b/packages/block-library/src/accordion-header/block.json index c70d8e29ca890e..3da3017f2b46a4 100644 --- a/packages/block-library/src/accordion-header/block.json +++ b/packages/block-library/src/accordion-header/block.json @@ -39,7 +39,6 @@ } }, "typography": { - "textAlign": true, "fontSize": true, "__experimentalFontFamily": true, "__experimentalFontWeight": true, @@ -72,10 +71,6 @@ "levelOptions": { "type": "array" }, - "textAlignment": { - "type": "string", - "default": "left" - }, "iconPosition": { "type": "string", "enum": [ "left", "right" ], diff --git a/packages/block-library/src/accordion-header/edit.js b/packages/block-library/src/accordion-header/edit.js index 0d8ba2f8226c9b..fbd5a4e3005543 100644 --- a/packages/block-library/src/accordion-header/edit.js +++ b/packages/block-library/src/accordion-header/edit.js @@ -1,7 +1,3 @@ -/** - * External dependencies - */ -import clsx from 'clsx'; /** * WordPress dependencies */ @@ -17,7 +13,7 @@ import { import { ToolbarGroup } from '@wordpress/components'; export default function Edit( { attributes, setAttributes, context } ) { - const { level, title, textAlign, levelOptions } = attributes; + const { level, title, levelOptions } = attributes; const { 'core/accordion-icon-position': iconPosition, 'core/accordion-show-icon': showIcon, @@ -34,11 +30,7 @@ export default function Edit( { attributes, setAttributes, context } ) { } }, [ iconPosition, showIcon, setAttributes ] ); - const blockProps = useBlockProps( { - className: clsx( 'accordion-content__heading', { - [ `has-text-align-${ textAlign }` ]: textAlign, - } ), - } ); + const blockProps = useBlockProps(); const spacingProps = useSpacingProps( attributes ); return ( @@ -56,7 +48,7 @@ export default function Edit( { attributes, setAttributes, context } ) { - + ); } diff --git a/packages/editor/src/components/collab-sidebar/comments.js b/packages/editor/src/components/collab-sidebar/comments.js index 36ce5882bc1000..a13f904d9c74e1 100644 --- a/packages/editor/src/components/collab-sidebar/comments.js +++ b/packages/editor/src/components/collab-sidebar/comments.js @@ -208,10 +208,7 @@ function Thread( { - + { if ( 'approved' === thread.status ) { @@ -229,21 +226,11 @@ function Thread( { event.stopPropagation(); // Prevent the parent onClick from being triggered clearThreadFocus(); } } - placeholderText={ - 'approved' === thread.status && - __( - 'Adding a comment will re-open this discussion….' - ) - } submitButtonText={ 'approved' === thread.status - ? _x( - 'Reopen & Reply', - 'Reopen comment and add reply' - ) - : _x( 'Reply', 'Add reply comment' ) + ? __( 'Reopen & Reply' ) + : __( 'Reply' ) } - rows={ 'approved' === thread.status ? 2 : 4 } /> diff --git a/packages/editor/src/components/collab-sidebar/style.scss b/packages/editor/src/components/collab-sidebar/style.scss index 84eab9e2de1494..e520c857f95320 100644 --- a/packages/editor/src/components/collab-sidebar/style.scss +++ b/packages/editor/src/components/collab-sidebar/style.scss @@ -34,15 +34,6 @@ box-shadow: 0 5.5px 7.8px -0.3px rgba(0, 0, 0, 0.102); } -.editor-collab-sidebar-panel__comment-field { - flex: 1; - - button { - flex-grow: 1; - justify-content: center; - } -} - .editor-collab-sidebar-panel__child-thread { margin-top: 15px; } @@ -132,6 +123,16 @@ font-weight: 500; } +.editor-collab-sidebar-panel__comment-form textarea { + @include input-control; + // Vertical padding is to match the standard 40px control height when rows=1, + // in conjunction with the 20px line-height. + // "Standard" metrics are 10px 12px, but subtracts 1px each to account for the border width. + padding: 9px 11px; + line-height: 20px !important; + display: block; +} + // Comment avatar indicators. .comment-avatar-indicator { position: relative; From c81d43ac1dfd9e348ba491495d4930fdea8ac7f9 Mon Sep 17 00:00:00 2001 From: Aki Hamano <54422211+t-hamano@users.noreply.github.com> Date: Fri, 26 Sep 2025 16:05:08 +0900 Subject: [PATCH 38/58] Block Comments: Apply border color to avatar (#71917) Co-authored-by: t-hamano Co-authored-by: Mamaduka --- .../collab-sidebar/comment-author-info.js | 15 ++++++++++- .../comment-indicator-toolbar.js | 9 ++++++- .../src/components/collab-sidebar/comments.js | 1 + .../src/components/collab-sidebar/style.scss | 3 +++ .../src/components/collab-sidebar/utils.js | 25 +++++++++++++++++++ 5 files changed, 51 insertions(+), 2 deletions(-) diff --git a/packages/editor/src/components/collab-sidebar/comment-author-info.js b/packages/editor/src/components/collab-sidebar/comment-author-info.js index 19221a12e4aa11..5456a6f9d7c5b6 100644 --- a/packages/editor/src/components/collab-sidebar/comment-author-info.js +++ b/packages/editor/src/components/collab-sidebar/comment-author-info.js @@ -13,6 +13,11 @@ import { store as coreStore } from '@wordpress/core-data'; import { useSelect } from '@wordpress/data'; import { store as blockEditorStore } from '@wordpress/block-editor'; +/** + * Internal dependencies + */ +import { getAvatarBorderColor } from './utils'; + /** * Render author information for a comment. * @@ -20,14 +25,16 @@ import { store as blockEditorStore } from '@wordpress/block-editor'; * @param {string} props.avatar - URL of the author's avatar. * @param {string} props.name - Name of the author. * @param {string} props.date - Date of the comment. + * @param {string} props.userId - User ID of the author. * * @return {React.ReactNode} The JSX element representing the author's information. */ -function CommentAuthorInfo( { avatar, name, date } ) { +function CommentAuthorInfo( { avatar, name, date, userId } ) { const dateSettings = getDateSettings(); const { currentUserAvatar, currentUserName, + currentUserId, dateFormat = dateSettings.formats.date, } = useSelect( ( select ) => { const { getCurrentUser, getEntityRecord } = select( coreStore ); @@ -39,6 +46,7 @@ function CommentAuthorInfo( { avatar, name, date } ) { return { currentUserAvatar: userData?.avatar_urls?.[ 48 ] ?? defaultAvatar, currentUserName: userData?.name, + currentUserId: userData?.id, dateFormat: siteSettings?.date_format, }; }, [] ); @@ -68,6 +76,11 @@ function CommentAuthorInfo( { avatar, name, date } ) { alt={ __( 'User avatar' ) } width={ 32 } height={ 32 } + style={ { + borderColor: getAvatarBorderColor( + userId ?? currentUserId + ), + } } /> diff --git a/packages/editor/src/components/collab-sidebar/comment-indicator-toolbar.js b/packages/editor/src/components/collab-sidebar/comment-indicator-toolbar.js index 81809edc036faa..9fbf78d3ff9b27 100644 --- a/packages/editor/src/components/collab-sidebar/comment-indicator-toolbar.js +++ b/packages/editor/src/components/collab-sidebar/comment-indicator-toolbar.js @@ -15,6 +15,7 @@ import clsx from 'clsx'; * Internal dependencies */ import { unlock } from '../../lock-unlock'; +import { getAvatarBorderColor } from './utils'; const { CommentIconToolbarSlotFill } = unlock( blockEditorPrivateApis ); @@ -40,6 +41,7 @@ const CommentAvatarIndicator = ( { onClick, thread, hasMoreComments } ) => { avatar: comment.author_avatar_urls?.[ '48' ] || comment.author_avatar_urls?.[ '96' ], + id: comment.author, isOriginalCommenter: comment.id === thread.id, date: comment.date, } ); @@ -107,7 +109,12 @@ const CommentAvatarIndicator = ( { onClick, thread, hasMoreComments } ) => { src={ participant.avatar } alt={ participant.name } className="comment-avatar" - style={ { zIndex: maxAvatars - index } } + style={ { + zIndex: maxAvatars - index, + borderColor: getAvatarBorderColor( + participant.id + ), + } } /> ) ) } { overflowCount > 0 && ( diff --git a/packages/editor/src/components/collab-sidebar/comments.js b/packages/editor/src/components/collab-sidebar/comments.js index a13f904d9c74e1..88316384f4c80b 100644 --- a/packages/editor/src/components/collab-sidebar/comments.js +++ b/packages/editor/src/components/collab-sidebar/comments.js @@ -291,6 +291,7 @@ const CommentBoard = ( { thread, onEdit, onDelete, status } ) => { avatar={ thread?.author_avatar_urls?.[ 48 ] } name={ thread?.author_name } date={ thread?.date } + userId={ thread?.author } /> diff --git a/packages/editor/src/components/collab-sidebar/style.scss b/packages/editor/src/components/collab-sidebar/style.scss index e520c857f95320..04e74f1f04b292 100644 --- a/packages/editor/src/components/collab-sidebar/style.scss +++ b/packages/editor/src/components/collab-sidebar/style.scss @@ -64,6 +64,9 @@ .editor-collab-sidebar-panel__user-avatar { border-radius: $radius-round; flex-shrink: 0; + border-width: var(--wp-admin-border-width-focus); + border-style: solid; + padding: var(--wp-admin-border-width-focus); } .editor-collab-sidebar-panel__thread-overlay { diff --git a/packages/editor/src/components/collab-sidebar/utils.js b/packages/editor/src/components/collab-sidebar/utils.js index 7e73344c5dc0e1..5746bbd3ffeeb0 100644 --- a/packages/editor/src/components/collab-sidebar/utils.js +++ b/packages/editor/src/components/collab-sidebar/utils.js @@ -7,3 +7,28 @@ export function sanitizeCommentString( str ) { return str.trim(); } + +/** + * These colors are picked from the WordPress.org design library. + * @see https://www.figma.com/design/HOJTpCFfa3tR0EccUlu0CM/WordPress.org-Design-Library?node-id=1-2193&t=M6WdRvTpt0mh8n6T-1 + */ +const AVATAR_BORDER_COLORS = [ + '#3858E9', // Blueberry + '#9fB1FF', // Blueberry 2 + '#1D35B4', // Dark Blueberry + '#1A1919', // Charcoal 0 + '#E26F56', // Pomegranate + '#33F078', // Acid Green + '#FFF972', // Lemon + '#7A00DF', // Purple +]; + +/** + * Gets the border color for an avatar based on the user ID. + * + * @param {number} userId - The user ID. + * @return {string} - The border color. + */ +export function getAvatarBorderColor( userId ) { + return AVATAR_BORDER_COLORS[ userId % AVATAR_BORDER_COLORS.length ]; +} From 352a020eba11f003d0c850345f9e5269fffe9779 Mon Sep 17 00:00:00 2001 From: Dave Smith Date: Fri, 26 Sep 2025 08:25:39 +0100 Subject: [PATCH 39/58] Fix Navigation Block default link consistency across all insertion methods (#71899) * Fix Navigation Block List View 'Add submenu link' to use default block attributes - Update AddSubmenuItem to use DEFAULT_BLOCK.name and DEFAULT_BLOCK.attributes - Ensures List View sidebar creates page-type links instead of custom links - Maintains consistency with in-canvas InnerBlocks and Add block inserter * Fix Navigation Submenu block InnerBlocks to use shared default block attributes - Import DEFAULT_BLOCK from Navigation Block constants - Remove local DEFAULT_BLOCK definition that was missing attributes - Ensures Navigation Submenu Add block inserter creates page-type links - Maintains consistency with main Navigation Block behavior Co-authored-by: getdave Co-authored-by: jeryj --- packages/block-library/src/navigation-submenu/edit.js | 5 +---- .../src/navigation/edit/leaf-more-menu.js | 10 +++++++++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/block-library/src/navigation-submenu/edit.js b/packages/block-library/src/navigation-submenu/edit.js index c59d6c579565ef..e6196261f75d63 100644 --- a/packages/block-library/src/navigation-submenu/edit.js +++ b/packages/block-library/src/navigation-submenu/edit.js @@ -46,6 +46,7 @@ import { getNavigationChildBlockProps, } from '../navigation/edit/utils'; import { useToolsPanelDropdownMenuProps } from '../utils/hooks'; +import { DEFAULT_BLOCK } from '../navigation/constants'; const ALLOWED_BLOCKS = [ 'core/navigation-link', @@ -53,10 +54,6 @@ const ALLOWED_BLOCKS = [ 'core/page-list', ]; -const DEFAULT_BLOCK = { - name: 'core/navigation-link', -}; - /** * A React hook to determine if it's dragging within the target element. * diff --git a/packages/block-library/src/navigation/edit/leaf-more-menu.js b/packages/block-library/src/navigation/edit/leaf-more-menu.js index 39fde1657666b2..ae44f007d9a158 100644 --- a/packages/block-library/src/navigation/edit/leaf-more-menu.js +++ b/packages/block-library/src/navigation/edit/leaf-more-menu.js @@ -13,6 +13,11 @@ import { useDispatch, useSelect } from '@wordpress/data'; import { __, sprintf } from '@wordpress/i18n'; import { BlockTitle, store as blockEditorStore } from '@wordpress/block-editor'; +/** + * Internal dependencies + */ +import { DEFAULT_BLOCK } from '../constants'; + const POPOVER_PROPS = { className: 'block-editor-block-settings-menu__popover', placement: 'bottom-start', @@ -43,7 +48,10 @@ function AddSubmenuItem( { disabled={ isDisabled } onClick={ () => { const updateSelectionOnInsert = false; - const newLink = createBlock( 'core/navigation-link' ); + const newLink = createBlock( + DEFAULT_BLOCK.name, + DEFAULT_BLOCK.attributes + ); if ( block.name === 'core/navigation-submenu' ) { insertBlock( From 13c3ecbe5dfd3727699acafdf3130c5346d18ace Mon Sep 17 00:00:00 2001 From: Karthick M <97787966+karthick-murugan@users.noreply.github.com> Date: Fri, 26 Sep 2025 12:56:38 +0530 Subject: [PATCH 40/58] Block Comments: Improve input labels (#71843) Co-authored-by: karthick-murugan Co-authored-by: Mamaduka Co-authored-by: t-hamano Co-authored-by: joedolson Co-authored-by: jasmussen Co-authored-by: jeffpaul --- .../src/components/collab-sidebar/add-comment.js | 5 ++++- .../src/components/collab-sidebar/comment-form.js | 11 +++++++++-- .../src/components/collab-sidebar/comments.js | 13 +++++++++++++ 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/packages/editor/src/components/collab-sidebar/add-comment.js b/packages/editor/src/components/collab-sidebar/add-comment.js index 9770d9d20819d6..c2734a997d4702 100644 --- a/packages/editor/src/components/collab-sidebar/add-comment.js +++ b/packages/editor/src/components/collab-sidebar/add-comment.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { _x } from '@wordpress/i18n'; +import { __, _x } from '@wordpress/i18n'; import { useSelect } from '@wordpress/data'; import { __experimentalHStack as HStack, @@ -53,6 +53,8 @@ export function AddComment( { return null; } + const commentLabel = __( 'New Comment' ); + return ( ); diff --git a/packages/editor/src/components/collab-sidebar/comment-form.js b/packages/editor/src/components/collab-sidebar/comment-form.js index 02ede3a8b5bbc7..263af128ce4951 100644 --- a/packages/editor/src/components/collab-sidebar/comment-form.js +++ b/packages/editor/src/components/collab-sidebar/comment-form.js @@ -30,9 +30,16 @@ import { sanitizeCommentString } from './utils'; * @param {Function} props.onCancel - The function to call when canceling the comment update. * @param {Object} props.thread - The comment thread object. * @param {string} props.submitButtonText - The text to display on the submit button. + * @param {string?} props.labelText - The label text for the comment input. * @return {React.ReactNode} The CommentForm component. */ -function CommentForm( { onSubmit, onCancel, thread, submitButtonText } ) { +function CommentForm( { + onSubmit, + onCancel, + thread, + submitButtonText, + labelText, +} ) { const [ inputComment, setInputComment ] = useState( thread?.content?.raw ?? '' ); @@ -48,7 +55,7 @@ function CommentForm( { onSubmit, onCancel, thread, submitButtonText } ) { spacing="4" > - { __( 'Comment' ) } + { labelText ?? __( 'Comment' ) } @@ -356,6 +363,12 @@ const CommentBoard = ( { thread, onEdit, onDelete, status } ) => { onCancel={ () => handleCancel() } thread={ thread } submitButtonText={ _x( 'Update', 'verb' ) } + labelText={ sprintf( + // translators: %1$s: comment identifier, %2$s: author name. + __( 'Edit Comment %1$s by %2$s' ), + thread.id, + thread?.author_name || 'Unknown' + ) } /> ) : ( From 8650614158a5452f89d3a7518aebdbc83b75290f Mon Sep 17 00:00:00 2001 From: Minal Diwan <38693713+theminaldiwan@users.noreply.github.com> Date: Fri, 26 Sep 2025 14:36:19 +0530 Subject: [PATCH 41/58] Accordion Panel: Fixes block visibility when the panel is collapsed (#71866) * Fixes block visibility when the panel is collapsed * Remove file from branch * update styles for accordion * update style for accordion panel * update style for accordion panel --------- Unlinked contributors: minaldiwan. Co-authored-by: theminaldiwan Co-authored-by: t-hamano Co-authored-by: shail-mehta --- packages/block-library/src/accordion-panel/style.scss | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/block-library/src/accordion-panel/style.scss b/packages/block-library/src/accordion-panel/style.scss index 3d898d6c3d0922..f58187fb953086 100644 --- a/packages/block-library/src/accordion-panel/style.scss +++ b/packages/block-library/src/accordion-panel/style.scss @@ -1,9 +1,8 @@ .wp-block-accordion-panel { - overflow: hidden; - // Prevent blockGap from Accordion Content block from adding extra margin between accordions. &[inert], &[aria-hidden="true"] { + display: none; margin-block-start: 0; } } From bd607b256efd8bede80394a324efb044c90dec1b Mon Sep 17 00:00:00 2001 From: David Arenas Date: Fri, 26 Sep 2025 13:03:48 +0200 Subject: [PATCH 42/58] iAPI: Fix nested `data-wp-each` directives using the same items key (#71870) * Add failing test * Shadow previous items with the same key * Fix race condition in context e2e tests * Fix phpcs error Co-authored-by: DAreRodz Co-authored-by: luisherranz --- .../directive-context/render.php | 8 ++++++++ .../directive-context/view.js | 18 +++++++++++++++++- .../directive-each/render.php | 12 ++++++++++++ packages/interactivity/src/directives.tsx | 8 ++++---- .../interactivity/directive-context.spec.ts | 10 ++++++++++ .../specs/interactivity/directive-each.spec.ts | 15 +++++++++++++++ 6 files changed, 66 insertions(+), 5 deletions(-) diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-context/render.php b/packages/e2e-tests/plugins/interactive-blocks/directive-context/render.php index 76635e37a26086..5fae2a9e66c9d9 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/directive-context/render.php +++ b/packages/e2e-tests/plugins/interactive-blocks/directive-context/render.php @@ -166,6 +166,14 @@
+ +
+
Async Navigate
`; -const { actions } = store( 'directive-context-navigate', { +const { actions, state } = store( 'directive-context-navigate', { + state: { + get navigationCount() { + const { __navigationCount } = state; + return isNaN( __navigationCount ) ? 0 : __navigationCount; + }, + __navigationCount: NaN, + }, actions: { toggleText() { const ctx = getContext(); @@ -99,6 +106,15 @@ const { actions } = store( 'directive-context-navigate', { ctx.newText = 'changed from async action'; }, }, + callbacks: { + updateNavigationCount() { + const { state: routerState } = store( 'core/router' ); + if ( routerState.url && isNaN( state.__navigationCount ) ) { + state.__navigationCount = 0; + } + state.__navigationCount++; + }, + }, } ); store( 'directive-context-watch', { diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-each/render.php b/packages/e2e-tests/plugins/interactive-blocks/directive-each/render.php index bfac62feb13595..325da4421f08c2 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/directive-each/render.php +++ b/packages/e2e-tests/plugins/interactive-blocks/directive-each/render.php @@ -311,3 +311,15 @@ > + +
    + +
diff --git a/packages/interactivity/src/directives.tsx b/packages/interactivity/src/directives.tsx index bd71507ef420fc..8a06c3df31439d 100644 --- a/packages/interactivity/src/directives.tsx +++ b/packages/interactivity/src/directives.tsx @@ -680,8 +680,11 @@ export default () => { const result: VNode< any >[] = []; for ( const item of iterable ) { + // Shadows a previous item with the same key. const itemContext = proxifyContext( - proxifyState( namespace, {} ), + proxifyState( namespace, { + [ itemProp ]: item, + } ), inheritedValue.client[ namespace ] ); const mergedContext = { @@ -692,9 +695,6 @@ export default () => { server: { ...inheritedValue.server }, }; - // Set the item after proxifying the context. - mergedContext.client[ namespace ][ itemProp ] = item; - const scope = { ...getScope(), context: mergedContext.client, diff --git a/test/e2e/specs/interactivity/directive-context.spec.ts b/test/e2e/specs/interactivity/directive-context.spec.ts index 0a27fe258d5a8d..b1e21b60710d8e 100644 --- a/test/e2e/specs/interactivity/directive-context.spec.ts +++ b/test/e2e/specs/interactivity/directive-context.spec.ts @@ -252,17 +252,24 @@ test.describe( 'data-wp-context', () => { page, } ) => { const element = page.getByTestId( 'navigation text' ); + const navCount = page.getByTestId( 'navigation count' ); + await expect( navCount ).toHaveText( '0' ); await page.getByTestId( 'navigate' ).click(); + await expect( navCount ).toHaveText( '1' ); await expect( element ).toHaveText( 'first page' ); await page.goBack(); + await expect( navCount ).toHaveText( '2' ); await expect( element ).toHaveText( 'first page' ); await page.goForward(); + await expect( navCount ).toHaveText( '3' ); await expect( element ).toHaveText( 'first page' ); } ); test( 'should inherit values on navigation', async ( { page } ) => { const text = page.getByTestId( 'navigation inherited text' ); const text2 = page.getByTestId( 'navigation inherited text2' ); + const navCount = page.getByTestId( 'navigation count' ); + await expect( navCount ).toHaveText( '0' ); await expect( text ).toHaveText( 'first page' ); await expect( text2 ).toBeEmpty(); await page.getByTestId( 'toggle text' ).click(); @@ -270,12 +277,15 @@ test.describe( 'data-wp-context', () => { await page.getByTestId( 'add text2' ).click(); await expect( text2 ).toHaveText( 'some new text' ); await page.getByTestId( 'navigate' ).click(); + await expect( navCount ).toHaveText( '1' ); await expect( text ).toHaveText( 'changed dynamically' ); await expect( text2 ).toHaveText( 'some new text' ); await page.goBack(); + await expect( navCount ).toHaveText( '2' ); await expect( text ).toHaveText( 'changed dynamically' ); await expect( text2 ).toHaveText( 'some new text' ); await page.goForward(); + await expect( navCount ).toHaveText( '3' ); await expect( text ).toHaveText( 'changed dynamically' ); await expect( text2 ).toHaveText( 'some new text' ); } ); diff --git a/test/e2e/specs/interactivity/directive-each.spec.ts b/test/e2e/specs/interactivity/directive-each.spec.ts index 3c015e63fe4bc1..ca3ed1c397b136 100644 --- a/test/e2e/specs/interactivity/directive-each.spec.ts +++ b/test/e2e/specs/interactivity/directive-each.spec.ts @@ -444,6 +444,21 @@ test.describe( 'data-wp-each', () => { } } ); + test( 'should support nested lists with the same item key', async ( { + page, + } ) => { + const mainElement = page.getByTestId( 'nested-with-same-item-key' ); + const listItems = mainElement.getByRole( 'listitem' ); + await expect( listItems ).toHaveText( [ + 'child1', + 'child2', + 'parent1', + 'child1', + 'child2', + 'parent2', + ] ); + } ); + test( 'should do nothing when used on non-template elements', async ( { page, } ) => { From a4959dc935112339b74058d1009e60c0b6f0f4b7 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Fri, 26 Sep 2025 12:06:02 +0100 Subject: [PATCH 43/58] DataViews: Expose FiltersToggled subcomponent. (#71907) --- packages/dataviews/CHANGELOG.md | 1 + packages/dataviews/README.md | 5 +- .../dataviews-filters/filters-toggled.tsx | 20 ++ .../components/dataviews-filters/filters.tsx | 73 +++++ .../components/dataviews-filters/index.tsx | 250 +----------------- .../components/dataviews-filters/toggle.tsx | 118 +++++++++ .../dataviews-filters/use-filters.ts | 73 +++++ .../src/components/dataviews-picker/index.tsx | 22 +- .../src/components/dataviews/index.tsx | 22 +- .../dataviews/stories/index.story.tsx | 2 +- 10 files changed, 309 insertions(+), 277 deletions(-) create mode 100644 packages/dataviews/src/components/dataviews-filters/filters-toggled.tsx create mode 100644 packages/dataviews/src/components/dataviews-filters/filters.tsx create mode 100644 packages/dataviews/src/components/dataviews-filters/toggle.tsx create mode 100644 packages/dataviews/src/components/dataviews-filters/use-filters.ts diff --git a/packages/dataviews/CHANGELOG.md b/packages/dataviews/CHANGELOG.md index 6d27b12a685a64..5aecaf5d1a9110 100644 --- a/packages/dataviews/CHANGELOG.md +++ b/packages/dataviews/CHANGELOG.md @@ -15,6 +15,7 @@ ### Enhancements - DataViews: Require at least one field to be visible. ([#71625](https://github.com/WordPress/gutenberg/pull/71625)) +- DataViews: Expose `DataViews.FiltersToggled` component to be used in free composition. [#71907](https://github.com/WordPress/gutenberg/pull/71907) ## 9.0.0 (2025-09-17) diff --git a/packages/dataviews/README.md b/packages/dataviews/README.md index 94409acfe85329..7d51bde6e63a62 100644 --- a/packages/dataviews/README.md +++ b/packages/dataviews/README.md @@ -452,6 +452,7 @@ The following components are available directly under `DataViews`: - `DataViews.Search` - `DataViews.FiltersToggle` +- `DataViews.FiltersToggled` - `DataViews.Filters` - `DataViews.Layout` - `DataViews.LayoutSwitcher` @@ -480,7 +481,7 @@ const CustomLayout = () => {

{ __( 'Free composition' ) }

- + @@ -492,7 +493,7 @@ const CustomLayout = () => { ### Accessibility considerations -All `DataViews` subcomponents are designed with accessibility in mind — including keyboard interactions, focus management, and semantic roles. Components like `Search`, `Pagination`, `FiltersToggle`, and `Filters` already handle these responsibilities internally and can be safely used in custom layouts. +All `DataViews` subcomponents are designed with accessibility in mind — including keyboard interactions, focus management, and semantic roles. Components like `Search`, `Pagination`, `FiltersToggle`, and `FiltersToggled` already handle these responsibilities internally and can be safely used in custom layouts. When using free composition, developers are responsible for the outer structure of the layout. diff --git a/packages/dataviews/src/components/dataviews-filters/filters-toggled.tsx b/packages/dataviews/src/components/dataviews-filters/filters-toggled.tsx new file mode 100644 index 00000000000000..12397e1bc3dcf4 --- /dev/null +++ b/packages/dataviews/src/components/dataviews-filters/filters-toggled.tsx @@ -0,0 +1,20 @@ +/** + * WordPress dependencies + */ +import { useContext } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import DataViewsContext from '../dataviews-context'; +import Filters from './filters'; + +function FiltersToggled( props: { className?: string } ) { + const { isShowingFilter } = useContext( DataViewsContext ); + if ( ! isShowingFilter ) { + return null; + } + return ; +} + +export default FiltersToggled; diff --git a/packages/dataviews/src/components/dataviews-filters/filters.tsx b/packages/dataviews/src/components/dataviews-filters/filters.tsx new file mode 100644 index 00000000000000..bba30c50479f16 --- /dev/null +++ b/packages/dataviews/src/components/dataviews-filters/filters.tsx @@ -0,0 +1,73 @@ +/** + * WordPress dependencies + */ +import { memo, useContext, useRef } from '@wordpress/element'; +import { __experimentalHStack as HStack } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import Filter from './filter'; +import { default as AddFilter } from './add-filter'; +import ResetFilters from './reset-filters'; +import useFilters from './use-filters'; +import DataViewsContext from '../dataviews-context'; + +function Filters( { className }: { className?: string } ) { + const { fields, view, onChangeView, openedFilter, setOpenedFilter } = + useContext( DataViewsContext ); + const addFilterRef = useRef< HTMLButtonElement >( null ); + const filters = useFilters( fields, view ); + const addFilter = ( + + ); + const visibleFilters = filters.filter( ( filter ) => filter.isVisible ); + if ( visibleFilters.length === 0 ) { + return null; + } + const filterComponents = [ + ...visibleFilters.map( ( filter ) => { + return ( + + ); + } ), + addFilter, + ]; + + filterComponents.push( + + ); + + return ( + + { filterComponents } + + ); +} + +export default memo( Filters ); diff --git a/packages/dataviews/src/components/dataviews-filters/index.tsx b/packages/dataviews/src/components/dataviews-filters/index.tsx index a473a3fa0d7376..1dff3ac4be157a 100644 --- a/packages/dataviews/src/components/dataviews-filters/index.tsx +++ b/packages/dataviews/src/components/dataviews-filters/index.tsx @@ -1,246 +1,4 @@ -/** - * WordPress dependencies - */ -import { - memo, - useContext, - useRef, - useMemo, - useCallback, - useEffect, -} from '@wordpress/element'; -import { __experimentalHStack as HStack, Button } from '@wordpress/components'; -import { funnel } from '@wordpress/icons'; -import { __, _x } from '@wordpress/i18n'; - -/** - * Internal dependencies - */ -import Filter from './filter'; -import { default as AddFilter, AddFilterMenu } from './add-filter'; -import ResetFilters from './reset-filters'; -import DataViewsContext from '../dataviews-context'; -import { ALL_OPERATORS, SINGLE_SELECTION_OPERATORS } from '../../constants'; -import type { NormalizedFilter, NormalizedField, View } from '../../types'; - -export function useFilters( fields: NormalizedField< any >[], view: View ) { - return useMemo( () => { - const filters: NormalizedFilter[] = []; - fields.forEach( ( field ) => { - if ( - field.filterBy === false || - ( ! field.elements?.length && ! field.Edit ) - ) { - return; - } - - const operators = field.filterBy.operators; - const isPrimary = !! field.filterBy?.isPrimary; - const isLocked = - view.filters?.some( - ( f ) => f.field === field.id && !! f.isLocked - ) ?? false; - filters.push( { - field: field.id, - name: field.label, - elements: field.elements ?? [], - singleSelection: operators.some( ( op ) => - SINGLE_SELECTION_OPERATORS.includes( op ) - ), - operators, - isVisible: - isLocked || - isPrimary || - !! view.filters?.some( - ( f ) => - f.field === field.id && - ALL_OPERATORS.includes( f.operator ) - ), - isPrimary, - isLocked, - } ); - } ); - - // Sort filters by: - // - locked filters go first - // - primary filters go next - // - then, sort by name - filters.sort( ( a, b ) => { - if ( a.isLocked && ! b.isLocked ) { - return -1; - } - if ( ! a.isLocked && b.isLocked ) { - return 1; - } - if ( a.isPrimary && ! b.isPrimary ) { - return -1; - } - if ( ! a.isPrimary && b.isPrimary ) { - return 1; - } - return a.name.localeCompare( b.name ); - } ); - return filters; - }, [ fields, view ] ); -} - -export function FiltersToggle() { - const { - filters, - view, - onChangeView, - setOpenedFilter, - isShowingFilter, - setIsShowingFilter, - } = useContext( DataViewsContext ); - - const buttonRef = useRef< HTMLButtonElement >( null ); - const onChangeViewWithFilterVisibility = useCallback( - ( _view: View ) => { - onChangeView( _view ); - setIsShowingFilter( true ); - }, - [ onChangeView, setIsShowingFilter ] - ); - const visibleFilters = filters.filter( ( filter ) => filter.isVisible ); - - const hasVisibleFilters = !! visibleFilters.length; - if ( filters.length === 0 ) { - return null; - } - - const addFilterButtonProps = { - label: __( 'Add filter' ), - 'aria-expanded': false, - isPressed: false, - }; - const toggleFiltersButtonProps = { - label: _x( 'Filter', 'verb' ), - 'aria-expanded': isShowingFilter, - isPressed: isShowingFilter, - onClick: () => { - if ( ! isShowingFilter ) { - setOpenedFilter( null ); - } - setIsShowingFilter( ! isShowingFilter ); - }, - }; - const buttonComponent = ( - + + + ); } return ( @@ -66,6 +138,7 @@ export default function TableControl< Item >( { { children.map( ( child ) => ( { child.label } ) ) } + { __( 'Actions' ) } @@ -89,11 +162,30 @@ export default function TableControl< Item >( { ) } ) ) } + + + ); } From 871cc11c4eb2373d6debcc2ddf499bb8c3fd72b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Maneiro?= <583546+oandregal@users.noreply.github.com> Date: Tue, 23 Sep 2025 11:52:37 +0200 Subject: [PATCH 53/58] Add story about dynamic data --- .../dataform/stories/index.story.tsx | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/packages/dataviews/src/components/dataform/stories/index.story.tsx b/packages/dataviews/src/components/dataform/stories/index.story.tsx index 65241409807a89..aedbc00108486a 100644 --- a/packages/dataviews/src/components/dataform/stories/index.story.tsx +++ b/packages/dataviews/src/components/dataform/stories/index.story.tsx @@ -1590,6 +1590,102 @@ export const LayoutMixed = { render: LayoutMixedComponent, }; +const DynamicDataComponent = () => { + type DynamicProduct = { + name: string; + cost: number; + quantity: number; + }; + type DynamicData = { + productList: DynamicProduct[]; + totalAmount: number; + }; + + const initialData = { + productList: [ + { + name: 'hair protection oil', + cost: 10, + quantity: 5, + }, + { + name: 'hair strength shampoo', + cost: 20, + quantity: 3, + }, + ], + totalAmount: 110, + }; + + const [ data, setData ] = useState< DynamicData >( initialData ); + + const form: Form = { + fields: [ + 'productList', + { + id: 'totalAmount', + label: 'Total Amount', + layout: { type: 'panel' }, + }, + ], + }; + + const _fields: Field< DynamicData >[] = [ + { + id: 'productList', + label: 'Product List', + type: 'array', + children: [ + { + id: 'name', + label: 'Name', + type: 'text', + }, + { + id: 'cost', + label: 'Cost', + type: 'integer', + }, + { + id: 'quantity', + label: 'Quantity', + type: 'integer', + }, + ], + }, + { + id: 'totalAmount', + label: 'Total Amount', + type: 'integer', + readOnly: true, + }, + ]; + + return ( + + data={ data } + form={ form } + fields={ _fields } + onChange={ ( edits ) => { + setData( ( prev ) => { + const updated = { ...prev, ...edits }; + return { + ...updated, + totalAmount: updated.productList.reduce( + ( acc, item ) => acc + item.cost * item.quantity, + 0 + ), + }; + } ); + } } + /> + ); +}; + +export const DynamicData = { + render: DynamicDataComponent, +}; + export const Validation = { render: ValidationComponent, argTypes: { From 6d07fcf0224acf2b12d76e6b00f29664443b4936 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Maneiro?= <583546+oandregal@users.noreply.github.com> Date: Tue, 23 Sep 2025 11:56:01 +0200 Subject: [PATCH 54/58] Better descriptions --- .../dataviews/src/dataform-controls/table.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/dataviews/src/dataform-controls/table.tsx b/packages/dataviews/src/dataform-controls/table.tsx index 11dc0a6a5291ed..276e2985952baa 100644 --- a/packages/dataviews/src/dataform-controls/table.tsx +++ b/packages/dataviews/src/dataform-controls/table.tsx @@ -46,7 +46,7 @@ export default function TableControl< Item >( { [ value, onChange, setValue, data ] ); - const addRow = useCallback( () => { + const addItem = useCallback( () => { const newRow: RowType = {}; if ( ! Array.isArray( children ) ) { return; @@ -63,7 +63,7 @@ export default function TableControl< Item >( { onChange( setValue( { item: data, value: updatedValue } ) ); }, [ children, value, onChange, setValue, data ] ); - const removeRow = useCallback( + const removeItem = useCallback( ( rowIndex: number ) => { const updatedValue = value.filter( ( _: RowType, index: number ) => index !== rowIndex @@ -113,11 +113,11 @@ export default function TableControl< Item >( {
@@ -165,8 +165,8 @@ export default function TableControl< Item >( { From 7f1b2d36d022f6ead872a8b0739b2f7c4bbb445b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Maneiro?= <583546+oandregal@users.noreply.github.com> Date: Tue, 23 Sep 2025 17:07:09 +0200 Subject: [PATCH 55/58] Update examples: panel --- .../dataform/stories/index.story.tsx | 49 +++++++++++++++++-- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/packages/dataviews/src/components/dataform/stories/index.story.tsx b/packages/dataviews/src/components/dataform/stories/index.story.tsx index aedbc00108486a..1b77f6598b583f 100644 --- a/packages/dataviews/src/components/dataform/stories/index.story.tsx +++ b/packages/dataviews/src/components/dataform/stories/index.story.tsx @@ -1620,12 +1620,44 @@ const DynamicDataComponent = () => { const [ data, setData ] = useState< DynamicData >( initialData ); const form: Form = { + layout: { type: 'card' }, fields: [ - 'productList', { - id: 'totalAmount', - label: 'Total Amount', - layout: { type: 'panel' }, + id: 'cardWithRegular', + children: [ + { + id: 'productListAsRegular', + label: 'Product list', + layout: { type: 'regular' }, + children: [ + 'productList', + { + id: 'totalAmount', + label: 'Total Amount', + layout: { type: 'panel' }, + }, + ], + }, + ], + }, + { + id: 'cardWithPanel', + children: [ + { + id: 'productListAsPanel', + label: 'Product list', + layout: { type: 'panel', openAs: 'modal' }, + summary: 'productListSummary', + children: [ + 'productList', + { + id: 'totalAmount', + label: 'Total Amount', + layout: { type: 'panel' }, + }, + ], + }, + ], }, ], }; @@ -1653,6 +1685,15 @@ const DynamicDataComponent = () => { }, ], }, + { + id: 'productListSummary', + label: 'Products', + type: 'text', + getValue: ( { item } ) => + item.productList + .map( ( product ) => product.name ) + .join( ', ' ), + }, { id: 'totalAmount', label: 'Total Amount', From bf33fbc99143cdca555b33529db0b450d0eb39f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Maneiro?= <583546+oandregal@users.noreply.github.com> Date: Tue, 23 Sep 2025 17:26:34 +0200 Subject: [PATCH 56/58] Update validation story --- .../dataform/stories/index.story.tsx | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/dataviews/src/components/dataform/stories/index.story.tsx b/packages/dataviews/src/components/dataform/stories/index.story.tsx index 1b77f6598b583f..a4d4c85cf376d9 100644 --- a/packages/dataviews/src/components/dataform/stories/index.story.tsx +++ b/packages/dataviews/src/components/dataform/stories/index.story.tsx @@ -903,23 +903,23 @@ const ValidationComponent = ( { const form = { layout: { type }, fields: [ - // 'text', - // 'select', - // 'textWithRadio', - // 'textarea', - // 'email', - // 'telephone', - // 'url', - // 'color', - // 'integer', - // 'boolean', - // 'categories', - // 'countries', - // 'toggle', - // 'toggleGroup', + 'text', + 'select', + 'textWithRadio', + 'textarea', + 'email', + 'telephone', + 'url', + 'color', + 'integer', + 'boolean', + 'categories', + 'countries', + 'toggle', + 'toggleGroup', 'arrayWithChildren', - // 'password', - // 'customEdit', + 'password', + 'customEdit', ], }; From 0a059ff1fb6d0feaf8fbb002f78d4c35cae373a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Maneiro?= <583546+oandregal@users.noreply.github.com> Date: Tue, 23 Sep 2025 17:32:59 +0200 Subject: [PATCH 57/58] Document children prop --- packages/dataviews/README.md | 30 ++++++++++++++++++++++++++++++ packages/dataviews/src/types.ts | 4 ++-- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/packages/dataviews/README.md b/packages/dataviews/README.md index 7d51bde6e63a62..afc132ce7ca975 100644 --- a/packages/dataviews/README.md +++ b/packages/dataviews/README.md @@ -1184,6 +1184,36 @@ Example: } ``` +### `children` + +Defines the type of the elements of the `array` field type. + +- Type: `'text' | Field[]` + - `'text'`: the elements of the array are of `text` type. + - `Field[]`: a list of children fields. +- Optional. + +If `children` is not provided for an `array` field type, the elements of the array will be of `text` type. + +Example: + +```js +{ + id: 'arrayWithChildren', + type: 'array', + children: [ + { + id: 'child1', + type: 'text' + }, + { + id: 'child2', + type: 'number' + } + ] +} +``` + ### `sort` Function to sort the records. diff --git a/packages/dataviews/src/types.ts b/packages/dataviews/src/types.ts index bf889020d88804..345393b7467a36 100644 --- a/packages/dataviews/src/types.ts +++ b/packages/dataviews/src/types.ts @@ -249,7 +249,7 @@ export type Field< Item > = { * Children field definitions for array fields with structured data. * When provided, array fields will render as tables with columns based on children. */ - children?: FieldType | Field< any >[]; + children?: 'text' | Field< any >[]; /** * Callback used to render the field. Defaults to `field.getValue`. @@ -336,7 +336,7 @@ export type NormalizedField< Item > = Omit< enableSorting: boolean; filterBy: NormalizedFilterByConfig | false; readOnly: boolean; - children: FieldType | NormalizedField< any >[]; + children: 'text' | NormalizedField< any >[]; }; /** From eb82ba34ca7826f1bf5c40d051ddca948e2ed2ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Maneiro?= <583546+oandregal@users.noreply.github.com> Date: Fri, 26 Sep 2025 15:04:33 +0200 Subject: [PATCH 58/58] Enable delete/add actions to be disabled and modify its labels --- .../dataform/stories/index.story.tsx | 7 +++ .../dataviews/src/dataform-controls/table.tsx | 53 ++++++++++++------- packages/dataviews/src/types.ts | 13 +++++ 3 files changed, 53 insertions(+), 20 deletions(-) diff --git a/packages/dataviews/src/components/dataform/stories/index.story.tsx b/packages/dataviews/src/components/dataform/stories/index.story.tsx index a4d4c85cf376d9..e590a6a90e079a 100644 --- a/packages/dataviews/src/components/dataform/stories/index.story.tsx +++ b/packages/dataviews/src/components/dataform/stories/index.story.tsx @@ -1667,6 +1667,13 @@ const DynamicDataComponent = () => { id: 'productList', label: 'Product List', type: 'array', + Edit: { + control: 'table', + actions: { + delete: 'Remove product', + add: 'Add product', + }, + }, children: [ { id: 'name', diff --git a/packages/dataviews/src/dataform-controls/table.tsx b/packages/dataviews/src/dataform-controls/table.tsx index 276e2985952baa..707f6838ee3143 100644 --- a/packages/dataviews/src/dataform-controls/table.tsx +++ b/packages/dataviews/src/dataform-controls/table.tsx @@ -18,9 +18,14 @@ export default function TableControl< Item >( { field, onChange, hideLabelFromVision, + config, }: DataFormControlProps< Item > ) { const { label, children, getValue, setValue } = field; const value = getValue( { item: data } ); + const { delete: deleteItemLabel, add: addItemLabel } = config?.actions || { + delete: 'Remove item', + add: 'Add item', + }; const onChangeRow = useCallback( ( @@ -138,7 +143,9 @@ export default function TableControl< Item >( { { children.map( ( child ) => ( { child.label } ) ) } - { __( 'Actions' ) } + { deleteItemLabel !== false && ( + { __( 'Actions' ) } + ) } @@ -162,30 +169,36 @@ export default function TableControl< Item >( { ) } ) ) } - - - + { addItemLabel !== false && ( +
+ +
+ ) } ); } diff --git a/packages/dataviews/src/types.ts b/packages/dataviews/src/types.ts index 345393b7467a36..8cbb0525f4af05 100644 --- a/packages/dataviews/src/types.ts +++ b/packages/dataviews/src/types.ts @@ -194,6 +194,14 @@ export type EditConfigText = { suffix?: React.ComponentType; }; +export type EditConfigTable = { + control: 'table'; + actions?: { + delete?: string | false; + add?: string | false; + }; +}; + /** * Edit configuration for other control types (excluding 'text' and 'textarea'). */ @@ -208,6 +216,7 @@ export type EditConfigGeneric = { export type EditConfig = | EditConfigTextarea | EditConfigText + | EditConfigTable | EditConfigGeneric; /** @@ -364,6 +373,10 @@ export type DataFormControlProps< Item > = { prefix?: React.ComponentType; suffix?: React.ComponentType; rows?: number; + actions?: { + delete?: string | false; + add?: string | false; + }; }; };