diff --git a/docs/reference-guides/core-blocks.md b/docs/reference-guides/core-blocks.md
index 16a5c8f607c1d..08367cb90f925 100644
--- a/docs/reference-guides/core-blocks.md
+++ b/docs/reference-guides/core-blocks.md
@@ -215,6 +215,15 @@ An advanced block that allows displaying post comments using different visual co
- **Supports:** align (full, wide), color (background, gradients, link, text), ~~html~~
- **Attributes:** tagName
+## Comments Title
+
+Displays a title with the number of comments ([Source](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-library/src/comments-title))
+
+- **Name:** core/comments-title
+- **Category:** theme
+- **Supports:** align, color (background, gradients, text), spacing (margin, padding), typography (fontSize, lineHeight), ~~anchor~~, ~~html~~
+- **Attributes:** level, multipleCommentsLabel, showCommentsCount, showPostTitle, singleCommentLabel, textAlign
+
## Cover
Add an image or video with a text overlay — great for headers. ([Source](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-library/src/cover))
diff --git a/lib/blocks.php b/lib/blocks.php
index 41ad13c442411..c7bf4e2d36c27 100644
--- a/lib/blocks.php
+++ b/lib/blocks.php
@@ -69,6 +69,7 @@ function gutenberg_reregister_core_block_types() {
'comments-pagination-next.php' => 'core/comments-pagination-next',
'comments-pagination-numbers.php' => 'core/comments-pagination-numbers',
'comments-pagination-previous.php' => 'core/comments-pagination-previous',
+ 'comments-title.php' => 'core/comments-title',
'file.php' => 'core/file',
'home-link.php' => 'core/home-link',
'image.php' => 'core/image',
diff --git a/packages/block-library/src/comments-query-loop/edit.js b/packages/block-library/src/comments-query-loop/edit.js
index 297811030f15e..8aa6d6a1155a5 100644
--- a/packages/block-library/src/comments-query-loop/edit.js
+++ b/packages/block-library/src/comments-query-loop/edit.js
@@ -9,6 +9,7 @@ import { useBlockProps, useInnerBlocksProps } from '@wordpress/block-editor';
import CommentsInspectorControls from './edit/comments-inspector-controls';
const TEMPLATE = [
+ [ 'core/comments-title' ],
[
'core/comment-template',
{},
diff --git a/packages/block-library/src/comments-title/block.json b/packages/block-library/src/comments-title/block.json
new file mode 100644
index 0000000000000..3ad6babf81e57
--- /dev/null
+++ b/packages/block-library/src/comments-title/block.json
@@ -0,0 +1,70 @@
+{
+ "$schema": "https://schemas.wp.org/trunk/block.json",
+ "apiVersion": 2,
+ "name": "core/comments-title",
+ "title": "Comments Title",
+ "category": "theme",
+ "ancestor": [ "core/comments-query-loop" ],
+ "description": "Displays a title with the number of comments",
+ "textdomain": "default",
+ "usesContext": [ "postId", "postType" ],
+ "attributes": {
+ "textAlign": {
+ "type": "string"
+ },
+ "singleCommentLabel": {
+ "type": "string"
+ },
+ "multipleCommentsLabel": {
+ "type": "string"
+ },
+ "showPostTitle": {
+ "type": "boolean",
+ "default": true
+ },
+ "showCommentsCount": {
+ "type": "boolean",
+ "default": true
+ },
+ "level": {
+ "type": "number",
+ "default": 2
+ }
+ },
+ "supports": {
+ "anchor": false,
+ "align": true,
+ "html": false,
+ "__experimentalBorder": {
+ "radius": true,
+ "color": true,
+ "width": true,
+ "style": true
+ },
+ "color": {
+ "gradients": true,
+ "__experimentalDefaultControls": {
+ "background": true,
+ "text": true
+ }
+ },
+ "spacing": {
+ "margin": true,
+ "padding": true
+ },
+ "typography": {
+ "fontSize": true,
+ "lineHeight": true,
+ "__experimentalFontStyle": true,
+ "__experimentalFontWeight": true,
+ "__experimentalFontFamily": true,
+ "__experimentalTextTransform": true,
+ "__experimentalDefaultControls": {
+ "fontSize": true,
+ "__experimentalFontFamily": true,
+ "__experimentalFontStyle": true,
+ "__experimentalFontWeight": true
+ }
+ }
+ }
+}
diff --git a/packages/block-library/src/comments-title/edit.js b/packages/block-library/src/comments-title/edit.js
new file mode 100644
index 0000000000000..e3855d60c2a56
--- /dev/null
+++ b/packages/block-library/src/comments-title/edit.js
@@ -0,0 +1,197 @@
+/**
+ * External dependencies
+ */
+import classnames from 'classnames';
+
+/**
+ * WordPress dependencies
+ */
+import {
+ AlignmentControl,
+ BlockControls,
+ useBlockProps,
+ PlainText,
+ InspectorControls,
+} from '@wordpress/block-editor';
+import { __ } from '@wordpress/i18n';
+import { useEntityProp } from '@wordpress/core-data';
+import {
+ PanelBody,
+ ToggleControl,
+ __experimentalToggleGroupControl as ToggleGroupControl,
+ __experimentalToggleGroupControlOption as ToggleGroupControlOption,
+} from '@wordpress/components';
+import { useState, useEffect } from '@wordpress/element';
+import apiFetch from '@wordpress/api-fetch';
+import { addQueryArgs } from '@wordpress/url';
+
+/**
+ * Internal dependencies
+ */
+import HeadingLevelDropdown from '../heading/heading-level-dropdown';
+
+export default function Edit( {
+ attributes: {
+ textAlign,
+ singleCommentLabel,
+ multipleCommentsLabel,
+ showPostTitle,
+ showCommentsCount,
+ level,
+ },
+ setAttributes,
+ context: { postType, postId },
+} ) {
+ const TagName = 'h' + level;
+ const [ commentsCount, setCommentsCount ] = useState();
+ const [ editingMode, setEditingMode ] = useState( 'plural' );
+ const [ rawTitle ] = useEntityProp( 'postType', postType, 'title', postId );
+ const isSiteEditor = typeof postId === 'undefined';
+ const blockProps = useBlockProps( {
+ className: classnames( {
+ [ `has-text-align-${ textAlign }` ]: textAlign,
+ } ),
+ } );
+
+ useEffect( () => {
+ if ( isSiteEditor ) {
+ setCommentsCount( 3 );
+ return;
+ }
+ const currentPostId = postId;
+ apiFetch( {
+ path: addQueryArgs( '/wp/v2/comments', {
+ post: postId,
+ _fields: 'id',
+ } ),
+ method: 'HEAD',
+ parse: false,
+ } )
+ .then( ( res ) => {
+ // Stale requests will have the `currentPostId` of an older closure.
+ if ( currentPostId === postId ) {
+ setCommentsCount(
+ parseInt( res.headers.get( 'X-WP-Total' ) )
+ );
+ }
+ } )
+ .catch( () => {
+ setCommentsCount( 0 );
+ } );
+ }, [ postId ] );
+
+ const blockControls = (
+
+
+ setAttributes( { textAlign: newAlign } )
+ }
+ />
+
+ setAttributes( { level: newLevel } )
+ }
+ />
+
+ );
+
+ const inspectorControls = (
+
+
+ { isSiteEditor && (
+
+
+
+
+ ) }
+
+ setAttributes( { showPostTitle: value } )
+ }
+ />
+
+ setAttributes( { showCommentsCount: value } )
+ }
+ />
+
+
+ );
+
+ const postTitle = isSiteEditor ? __( '"Post Title"' ) : `"${ rawTitle }"`;
+
+ const singlePlaceholder = showPostTitle
+ ? __( 'One response to ' )
+ : __( 'One response' );
+
+ const multiplePlaceholder = showPostTitle
+ ? __( 'Responses to ' )
+ : __( 'Responses' );
+
+ return (
+ <>
+ { blockControls }
+ { inspectorControls }
+
+ { editingMode === 'singular' || commentsCount === 1 ? (
+ <>
+
+ setAttributes( {
+ singleCommentLabel: newLabel,
+ } )
+ }
+ />
+ { showPostTitle ? postTitle : null }
+ >
+ ) : (
+ <>
+ { showCommentsCount ? commentsCount : null }
+
+ setAttributes( {
+ multipleCommentsLabel: newLabel,
+ } )
+ }
+ />
+ { showPostTitle ? postTitle : null }
+ >
+ ) }
+
+ >
+ );
+}
diff --git a/packages/block-library/src/comments-title/editor.scss b/packages/block-library/src/comments-title/editor.scss
new file mode 100644
index 0000000000000..6f9d964bbb9c6
--- /dev/null
+++ b/packages/block-library/src/comments-title/editor.scss
@@ -0,0 +1,4 @@
+
+.wp-block-comments-title.has-background {
+ padding: inherit;
+}
diff --git a/packages/block-library/src/comments-title/index.js b/packages/block-library/src/comments-title/index.js
new file mode 100644
index 0000000000000..988be3ef86169
--- /dev/null
+++ b/packages/block-library/src/comments-title/index.js
@@ -0,0 +1,18 @@
+/**
+ * WordPress dependencies
+ */
+import { commentTitle as icon } from '@wordpress/icons';
+
+/**
+ * Internal dependencies
+ */
+import metadata from './block.json';
+import edit from './edit';
+
+const { name } = metadata;
+export { metadata, name };
+
+export const settings = {
+ icon,
+ edit,
+};
diff --git a/packages/block-library/src/comments-title/index.php b/packages/block-library/src/comments-title/index.php
new file mode 100644
index 0000000000000..ed95541b7ecb0
--- /dev/null
+++ b/packages/block-library/src/comments-title/index.php
@@ -0,0 +1,68 @@
+ $align_class_name ) );
+ $post_title = $show_post_title ? sprintf( '"%1$s"', get_the_title() ) : null;
+ $comments_count = number_format_i18n( get_comments_number() );
+ $tag_name = 'h2';
+ if ( isset( $attributes['level'] ) ) {
+ $tag_name = 'h' . $attributes['level'];
+ }
+
+ if ( '0' === $comments_count ) {
+ return;
+ }
+
+ $single_default_comment_label = $show_post_title ? __( 'One response to' ) : __( 'One response' );
+ $single_comment_label = ! empty( $attributes['singleCommentLabel'] ) ? $attributes['singleCommentLabel'] : $single_default_comment_label;
+
+ $multiple_default_comment_label = $show_post_title ? __( 'Responses to' ) : __( 'Responses' );
+ $multiple_comment_label = ! empty( $attributes['multipleCommentsLabel'] ) ? $attributes['multipleCommentsLabel'] : $multiple_default_comment_label;
+
+ $comments_title = '%1$s %2$s %3$s';
+
+ $comments_title = sprintf(
+ $comments_title,
+ // If there is only one comment, only display the label.
+ '1' !== $comments_count && $show_comments_count ? $comments_count : null,
+ '1' === $comments_count ? $single_comment_label : $multiple_comment_label,
+ $post_title
+ );
+
+ return sprintf(
+ '<%1$s id="comments" %2$s>%3$s%1$s>',
+ $tag_name,
+ $wrapper_attributes,
+ $comments_title
+ );
+}
+
+ /**
+ * Registers the `core/comments-title` block on the server.
+ */
+function register_block_core_comments_title() {
+ register_block_type_from_metadata(
+ __DIR__ . '/comments-title',
+ array(
+ 'render_callback' => 'render_block_core_comments_title',
+ )
+ );
+}
+
+ add_action( 'init', 'register_block_core_comments_title' );
diff --git a/packages/block-library/src/editor.scss b/packages/block-library/src/editor.scss
index 59c1f993a51e1..ff322e97d467f 100644
--- a/packages/block-library/src/editor.scss
+++ b/packages/block-library/src/editor.scss
@@ -10,6 +10,7 @@
@import "./comments-query-loop/editor.scss";
@import "./comments-pagination/editor.scss";
@import "./comments-pagination-numbers/editor.scss";
+@import "./comments-title/editor.scss";
@import "./cover/editor.scss";
@import "./embed/editor.scss";
@import "./file/editor.scss";
diff --git a/packages/block-library/src/index.js b/packages/block-library/src/index.js
index 1fe0de453d4e6..03e79846060c0 100644
--- a/packages/block-library/src/index.js
+++ b/packages/block-library/src/index.js
@@ -35,6 +35,7 @@ import * as commentsQueryLoop from './comments-query-loop';
import * as commentsPagination from './comments-pagination';
import * as commentsPaginationNext from './comments-pagination-next';
import * as commentsPaginationNumbers from './comments-pagination-numbers';
+import * as commentsTitle from './comments-title';
import * as cover from './cover';
import * as embed from './embed';
import * as file from './file';
@@ -212,11 +213,13 @@ export const __experimentalGetCoreBlocks = () => [
commentEditLink,
commentReplyLink,
commentTemplate,
+ commentsTitle,
commentsQueryLoop,
commentsPagination,
commentsPaginationNext,
commentsPaginationNumbers,
commentsPaginationPrevious,
+
postComments,
homeLink,
logInOut,
diff --git a/packages/icons/CHANGELOG.md b/packages/icons/CHANGELOG.md
index 4655d0ee7a5a3..afb1ace38c3e7 100644
--- a/packages/icons/CHANGELOG.md
+++ b/packages/icons/CHANGELOG.md
@@ -2,6 +2,8 @@
## Unreleased
+- Add new `commentTitle` icon. ([#40419](https://github.com/WordPress/gutenberg/pull/40419))
+
## 8.2.0 (2022-04-08)
### New Features
diff --git a/packages/icons/src/index.js b/packages/icons/src/index.js
index 32d4cfe14bfdf..231dbf6ac6a56 100644
--- a/packages/icons/src/index.js
+++ b/packages/icons/src/index.js
@@ -54,6 +54,7 @@ export { default as commentAuthorName } from './library/comment-author-name';
export { default as commentContent } from './library/comment-content';
export { default as commentReplyLink } from './library/comment-reply-link';
export { default as commentEditLink } from './library/comment-edit-link';
+export { default as commentTitle } from './library/comment-title';
export { default as cover } from './library/cover';
export { default as create } from './library/create';
export { default as crop } from './library/crop';
diff --git a/packages/icons/src/library/comment-title.js b/packages/icons/src/library/comment-title.js
new file mode 100644
index 0000000000000..dd119dea0e03d
--- /dev/null
+++ b/packages/icons/src/library/comment-title.js
@@ -0,0 +1,12 @@
+/**
+ * WordPress dependencies
+ */
+import { Path, SVG } from '@wordpress/primitives';
+
+const commentTitle = (
+
+);
+
+export default commentTitle;
diff --git a/test/integration/fixtures/blocks/core__comments-query-loop.html b/test/integration/fixtures/blocks/core__comments-query-loop.html
index 2fbc1e0de80de..156166ee90f9a 100644
--- a/test/integration/fixtures/blocks/core__comments-query-loop.html
+++ b/test/integration/fixtures/blocks/core__comments-query-loop.html
@@ -1,41 +1,33 @@
-
+
\ No newline at end of file
diff --git a/test/integration/fixtures/blocks/core__comments-query-loop.json b/test/integration/fixtures/blocks/core__comments-query-loop.json
index 98e97afdb9c90..d796d78c97ffd 100644
--- a/test/integration/fixtures/blocks/core__comments-query-loop.json
+++ b/test/integration/fixtures/blocks/core__comments-query-loop.json
@@ -6,6 +6,34 @@
"tagName": "div"
},
"innerBlocks": [
+ {
+ "name": "core/comments-title",
+ "isValid": true,
+ "attributes": {
+ "showPostTitle": true,
+ "showCommentsCount": true,
+ "level": 4,
+ "borderColor": "vivid-red",
+ "backgroundColor": "primary",
+ "textColor": "background",
+ "fontSize": "large",
+ "style": {
+ "spacing": {
+ "padding": {
+ "top": "6px",
+ "right": "6px",
+ "bottom": "6px",
+ "left": "6px"
+ }
+ },
+ "border": {
+ "width": "3px",
+ "radius": "100px"
+ }
+ }
+ },
+ "innerBlocks": []
+ },
{
"name": "core/comment-template",
"isValid": true,
diff --git a/test/integration/fixtures/blocks/core__comments-query-loop.parsed.json b/test/integration/fixtures/blocks/core__comments-query-loop.parsed.json
index a5f207fff8388..529336e78a098 100644
--- a/test/integration/fixtures/blocks/core__comments-query-loop.parsed.json
+++ b/test/integration/fixtures/blocks/core__comments-query-loop.parsed.json
@@ -3,6 +3,33 @@
"blockName": "core/comments-query-loop",
"attrs": {},
"innerBlocks": [
+ {
+ "blockName": "core/comments-title",
+ "attrs": {
+ "level": 4,
+ "style": {
+ "spacing": {
+ "padding": {
+ "top": "6px",
+ "right": "6px",
+ "bottom": "6px",
+ "left": "6px"
+ }
+ },
+ "border": {
+ "width": "3px",
+ "radius": "100px"
+ }
+ },
+ "borderColor": "vivid-red",
+ "backgroundColor": "primary",
+ "textColor": "background",
+ "fontSize": "large"
+ },
+ "innerBlocks": [],
+ "innerHTML": "",
+ "innerContent": []
+ },
{
"blockName": "core/comment-template",
"attrs": {},
@@ -32,11 +59,11 @@
"innerContent": []
}
],
- "innerHTML": "\n\t\t\n\t\t\t\n\t\t
\n\t\t",
+ "innerHTML": "\n\t\n\t",
"innerContent": [
- "\n\t\t\n\t\t\t",
+ "\n\t
",
null,
- "\n\t\t
\n\t\t"
+ "
\n\t"
]
},
{
@@ -81,13 +108,13 @@
"innerContent": []
}
],
- "innerHTML": "\n\t\t\t\n\t\t\t\t\n\n\t\t\t\t\n\t\t\t
\n\t\t\t",
+ "innerHTML": "\n\t\n\t\n\t
\n\t",
"innerContent": [
- "\n\t\t\t\n\t\t\t\t",
+ "\n\t
",
null,
- "\n\n\t\t\t\t",
+ "\n\t\n\t",
null,
- "\n\t\t\t
\n\t\t\t"
+ "
\n\t"
]
},
{
@@ -105,27 +132,27 @@
"innerContent": []
}
],
- "innerHTML": "\n\t\t\n\t\t\t\n\n\t\t\t\n\n\t\t\t\n\n\t\t\t\n\t\t
\n\t\t",
+ "innerHTML": "\n\t\n\t\n\t\n\t\n\t\n\t\n\t
\n\t",
"innerContent": [
- "\n\t\t\n\t\t\t",
+ "\n\t
",
null,
- "\n\n\t\t\t",
+ "\n\t\n\t",
null,
- "\n\n\t\t\t",
+ "\n\t\n\t",
null,
- "\n\n\t\t\t",
+ "\n\t\n\t",
null,
- "\n\t\t
\n\t\t"
+ "
\n\t"
]
}
],
- "innerHTML": "\n\t\n\t\t\n\n\t\t\n\t
\n\t",
+ "innerHTML": "\n\t\n\t\n\t
\n\t",
"innerContent": [
- "\n\t\n\t\t",
+ "\n\t
",
null,
- "\n\n\t\t",
+ "\n\t\n\t",
null,
- "\n\t
\n\t"
+ "
\n\t"
]
}
],
@@ -158,25 +185,27 @@
"innerContent": []
}
],
- "innerHTML": "\n\t\n\n\t\n\n\t\n\t",
+ "innerHTML": "\n\t\n\t\n\t\n\t\n\t\n\t",
"innerContent": [
"\n\t",
null,
- "\n\n\t",
+ "\n\t\n\t",
null,
- "\n\n\t",
+ "\n\t\n\t",
null,
"\n\t"
]
}
],
- "innerHTML": "\n\n",
+ "innerHTML": "\n\n\t",
"innerContent": [
- "\n\n\t"
]
}
]
diff --git a/test/integration/fixtures/blocks/core__comments-query-loop.serialized.html b/test/integration/fixtures/blocks/core__comments-query-loop.serialized.html
index 014b6210aaa08..dd0aa7f091628 100644
--- a/test/integration/fixtures/blocks/core__comments-query-loop.serialized.html
+++ b/test/integration/fixtures/blocks/core__comments-query-loop.serialized.html
@@ -1,5 +1,7 @@
-