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 ( +
    +
      + { tabsTitle } +
    + { tabsContent } +
    + ); +}; + +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 ); + } ); +} );