diff --git a/assets/js/atomic/blocks/index.js b/assets/js/atomic/blocks/index.js
index 6745263834d..29997853322 100644
--- a/assets/js/atomic/blocks/index.js
+++ b/assets/js/atomic/blocks/index.js
@@ -14,3 +14,4 @@ import './product-elements/tag-list';
import './product-elements/stock-indicator';
import './product-elements/add-to-cart';
import './product-elements/product-image-gallery';
+import './product-elements/product-details';
diff --git a/assets/js/atomic/blocks/product-elements/product-details/block.json b/assets/js/atomic/blocks/product-elements/product-details/block.json
new file mode 100644
index 00000000000..02deaacac05
--- /dev/null
+++ b/assets/js/atomic/blocks/product-elements/product-details/block.json
@@ -0,0 +1,14 @@
+{
+ "name": "woocommerce/product-details",
+ "version": "1.0.0",
+ "icon": "info",
+ "title": "Product Details",
+ "description": "A block that allows your customers to see details and reviews about the product.",
+ "category": "woocommerce",
+ "keywords": [ "WooCommerce" ],
+ "supports": {},
+ "attributes": {},
+ "textdomain": "woo-gutenberg-products-block",
+ "apiVersion": 2,
+ "$schema": "https://schemas.wp.org/trunk/block.json"
+}
diff --git a/assets/js/atomic/blocks/product-elements/product-details/block.tsx b/assets/js/atomic/blocks/product-elements/product-details/block.tsx
new file mode 100644
index 00000000000..37db65931f0
--- /dev/null
+++ b/assets/js/atomic/blocks/product-elements/product-details/block.tsx
@@ -0,0 +1,95 @@
+/**
+ * External dependencies
+ */
+import classnames from 'classnames';
+import { __ } from '@wordpress/i18n';
+import { useBlockProps } from '@wordpress/block-editor';
+
+/**
+ * Internal dependencies
+ */
+
+interface SingleProductTab {
+ id: string;
+ title: string;
+ active: boolean;
+ content: string | undefined;
+}
+
+const ProductTabTitle = ( {
+ id,
+ title,
+ active,
+}: Pick< SingleProductTab, 'id' | 'title' | 'active' > ) => {
+ return (
+
+ { title }
+
+ );
+};
+
+const ProductTabContent = ( {
+ id,
+ content,
+}: Pick< SingleProductTab, 'id' | 'content' > ) => {
+ return (
+
+ { content }
+
+ );
+};
+
+export const SingleProductDetails = () => {
+ const blockProps = useBlockProps();
+ const productTabs = [
+ {
+ id: 'description',
+ title: 'Description',
+ active: true,
+ content: __(
+ 'This block lists description, attributes and reviews for a single product.',
+ 'woo-gutenberg-products-block'
+ ),
+ },
+ {
+ id: 'additional_information',
+ title: 'Additional Information',
+ active: false,
+ },
+ { id: 'reviews', title: 'Reviews', active: false },
+ ];
+ const tabsTitle = productTabs.map( ( { id, title, active } ) => (
+
+ ) );
+ const tabsContent = productTabs.map( ( { id, content } ) => (
+
+ ) );
+
+ return (
+
+ );
+};
+
+export default SingleProductDetails;
diff --git a/assets/js/atomic/blocks/product-elements/product-details/edit.tsx b/assets/js/atomic/blocks/product-elements/product-details/edit.tsx
new file mode 100644
index 00000000000..6b8e4ab9f9c
--- /dev/null
+++ b/assets/js/atomic/blocks/product-elements/product-details/edit.tsx
@@ -0,0 +1,31 @@
+/**
+ * External dependencies
+ */
+import { useBlockProps } from '@wordpress/block-editor';
+import { Disabled } from '@wordpress/components';
+import type { BlockEditProps } from '@wordpress/blocks';
+
+/**
+ * Internal dependencies
+ */
+import Block from './block';
+import { Attributes } from './types';
+
+const Edit = ( { attributes }: BlockEditProps< Attributes > ) => {
+ const { className } = attributes;
+ const blockProps = useBlockProps( {
+ className,
+ } );
+
+ return (
+ <>
+
+
+
+
+
+ >
+ );
+};
+
+export default Edit;
diff --git a/assets/js/atomic/blocks/product-elements/product-details/index.tsx b/assets/js/atomic/blocks/product-elements/product-details/index.tsx
new file mode 100644
index 00000000000..6d2ec77578e
--- /dev/null
+++ b/assets/js/atomic/blocks/product-elements/product-details/index.tsx
@@ -0,0 +1,24 @@
+/**
+ * External dependencies
+ */
+import { registerBlockSingleProductTemplate } from '@woocommerce/atomic-utils';
+import { registerBlockType, unregisterBlockType } from '@wordpress/blocks';
+
+/**
+ * Internal dependencies
+ */
+import metadata from './block.json';
+import edit from './edit';
+
+registerBlockSingleProductTemplate( {
+ registerBlockFn: () => {
+ // @ts-expect-error: `registerBlockType` is a function that is typed in WordPress core.
+ registerBlockType( metadata, {
+ edit,
+ } );
+ },
+ unregisterBlockFn: () => {
+ unregisterBlockType( metadata.name );
+ },
+ blockName: metadata.name,
+} );
diff --git a/assets/js/atomic/blocks/product-elements/product-details/style.scss b/assets/js/atomic/blocks/product-elements/product-details/style.scss
new file mode 100644
index 00000000000..07b5c8031f0
--- /dev/null
+++ b/assets/js/atomic/blocks/product-elements/product-details/style.scss
@@ -0,0 +1,46 @@
+.wp-block-woocommerce-product-details {
+ ul.tabs {
+ list-style: none;
+ padding: 0 0 0 1em;
+ margin: 0 0 1.618em;
+ overflow: hidden;
+ position: relative;
+ border-bottom: 1px solid $gray-200;
+
+ li {
+ border: 1px solid $gray-200;
+ background-color: $white;
+ display: inline-block;
+ position: relative;
+ z-index: 0;
+ border-radius: 4px 4px 0 0;
+ margin: 0;
+ padding: 0.5em 1em;
+ opacity: 0.5;
+
+ a {
+ display: inline-block;
+ font-weight: 700;
+ color: $black;
+ text-decoration: none;
+
+ &:hover {
+ text-decoration: none;
+ color: color.adjust($black, $lightness: 10%);
+ }
+ }
+
+ &.active {
+ background: $gray-100;
+ z-index: 2;
+ border-bottom-color: $gray-200;
+ opacity: 1;
+
+ a {
+ color: inherit;
+ text-shadow: inherit;
+ }
+ }
+ }
+ }
+}
diff --git a/assets/js/atomic/blocks/product-elements/product-details/types.ts b/assets/js/atomic/blocks/product-elements/product-details/types.ts
new file mode 100644
index 00000000000..aa9b37b316a
--- /dev/null
+++ b/assets/js/atomic/blocks/product-elements/product-details/types.ts
@@ -0,0 +1,3 @@
+export interface Attributes {
+ className?: string;
+}
diff --git a/src/BlockTemplatesController.php b/src/BlockTemplatesController.php
index 06a339b779f..6b3b42f8f8f 100644
--- a/src/BlockTemplatesController.php
+++ b/src/BlockTemplatesController.php
@@ -67,7 +67,6 @@ protected function init() {
add_filter( 'get_block_templates', array( $this, 'add_block_templates' ), 10, 3 );
add_filter( 'current_theme_supports-block-templates', array( $this, 'remove_block_template_support_for_shop_page' ) );
add_filter( 'taxonomy_template_hierarchy', array( $this, 'add_archive_product_to_eligible_for_fallback_templates' ), 10, 1 );
-
if ( $this->package->is_experimental_build() ) {
add_action( 'after_switch_theme', array( $this, 'check_should_use_blockified_product_grid_templates' ), 10, 2 );
}
diff --git a/src/BlockTypes/ProductDetails.php b/src/BlockTypes/ProductDetails.php
new file mode 100644
index 00000000000..3a1795279e5
--- /dev/null
+++ b/src/BlockTypes/ProductDetails.php
@@ -0,0 +1,63 @@
+render_tabs();
+
+ $classname = $attributes['className'] ?? '';
+
+ return sprintf(
+ '
+ %2$s
+
',
+ esc_attr( $classname ),
+ $tabs
+ );
+ }
+
+ /**
+ * Gets the tabs with their content to be rendered by the block.
+ *
+ * @return string The tabs html to be rendered by the block
+ */
+ protected function render_tabs() {
+ ob_start();
+
+ while ( have_posts() ) {
+ the_post();
+ woocommerce_output_product_data_tabs();
+ }
+
+ $tabs = ob_get_clean();
+
+ return $tabs;
+ }
+}
diff --git a/src/BlockTypes/ProductImageGallery.php b/src/BlockTypes/ProductImageGallery.php
index 43e84ebbe32..cb97f7ed331 100644
--- a/src/BlockTypes/ProductImageGallery.php
+++ b/src/BlockTypes/ProductImageGallery.php
@@ -24,6 +24,13 @@ protected function get_block_type_uses_context() {
return [ 'query', 'queryId', 'postId' ];
}
+ /**
+ * It isn't necessary register block assets because it is a server side block.
+ */
+ protected function register_block_type_assets() {
+ return null;
+ }
+
/**
* Include and render the block.
diff --git a/src/BlockTypesController.php b/src/BlockTypesController.php
index e9355759ec0..653b766daf2 100644
--- a/src/BlockTypesController.php
+++ b/src/BlockTypesController.php
@@ -207,6 +207,7 @@ protected function get_block_types() {
'RatingFilter',
'ReviewsByCategory',
'ReviewsByProduct',
+ 'ProductDetails',
'StockFilter',
];
@@ -255,6 +256,7 @@ protected function get_block_types() {
'CatalogSorting',
'ClassicTemplate',
'ProductResultsCount',
+ 'ProductDetails',
'StoreNotices',
]
);
diff --git a/tests/e2e/config/custom-matchers/__fixtures__/single-product-details.fixture.json b/tests/e2e/config/custom-matchers/__fixtures__/single-product-details.fixture.json
new file mode 100644
index 00000000000..1df5149a095
--- /dev/null
+++ b/tests/e2e/config/custom-matchers/__fixtures__/single-product-details.fixture.json
@@ -0,0 +1 @@
+{"title":"Product Details Block","pageContent":""}
diff --git a/tests/e2e/specs/backend/single-produt-details.test.js b/tests/e2e/specs/backend/single-produt-details.test.js
new file mode 100644
index 00000000000..81eabe5fa40
--- /dev/null
+++ b/tests/e2e/specs/backend/single-produt-details.test.js
@@ -0,0 +1,32 @@
+/**
+ * External dependencies
+ */
+import { getAllBlocks, switchUserToAdmin } from '@wordpress/e2e-test-utils';
+import { visitBlockPage } from '@woocommerce/blocks-test-utils';
+
+/**
+ * Internal dependencies
+ */
+import { insertBlockDontWaitForInsertClose } from '../../utils.js';
+
+const block = {
+ name: 'Product Details',
+ slug: 'woocommerce/single-product-details',
+ class: '.wc-block-single-product-details',
+};
+
+describe( `${ block.name } Block`, () => {
+ beforeAll( async () => {
+ await switchUserToAdmin();
+ await visitBlockPage( `${ block.name } Block` );
+ } );
+
+ it( 'can be inserted more than once', async () => {
+ await insertBlockDontWaitForInsertClose( block.name );
+ expect( await getAllBlocks() ).toHaveLength( 2 );
+ } );
+
+ it( 'renders without crashing', async () => {
+ await expect( page ).toRenderBlock( block );
+ } );
+} );