diff --git a/packages/js/product-editor/changelog/add-37096 b/packages/js/product-editor/changelog/add-37096
new file mode 100644
index 00000000000..3cf7902aef5
--- /dev/null
+++ b/packages/js/product-editor/changelog/add-37096
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Add tabs block and tabs to product editor
diff --git a/packages/js/product-editor/package.json b/packages/js/product-editor/package.json
index 5ea325f9090..677e915c674 100644
--- a/packages/js/product-editor/package.json
+++ b/packages/js/product-editor/package.json
@@ -34,6 +34,7 @@
"@woocommerce/components": "workspace:*",
"@woocommerce/currency": "workspace:*",
"@woocommerce/data": "workspace:^4.1.0",
+ "@woocommerce/navigation": "workspace:^8.1.0",
"@woocommerce/number": "workspace:*",
"@woocommerce/tracks": "workspace:^1.3.0",
"@wordpress/block-editor": "^9.8.0",
diff --git a/packages/js/product-editor/src/components/block-editor/block-editor.tsx b/packages/js/product-editor/src/components/block-editor/block-editor.tsx
index db5b062a707..f0935a2c823 100644
--- a/packages/js/product-editor/src/components/block-editor/block-editor.tsx
+++ b/packages/js/product-editor/src/components/block-editor/block-editor.tsx
@@ -2,7 +2,12 @@
* External dependencies
*/
import { Template } from '@wordpress/blocks';
-import { createElement, useMemo, useLayoutEffect } from '@wordpress/element';
+import {
+ createElement,
+ useMemo,
+ useLayoutEffect,
+ useState,
+} from '@wordpress/element';
import { Product } from '@woocommerce/data';
import { useSelect, select as WPSelect, useDispatch } from '@wordpress/data';
import { uploadMedia } from '@wordpress/media-utils';
@@ -10,6 +15,9 @@ import {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore No types for this exist yet.
BlockBreadcrumb,
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore No types for this exist yet.
+ BlockContextProvider,
BlockEditorKeyboardShortcuts,
BlockEditorProvider,
BlockList,
@@ -34,6 +42,7 @@ import {
* Internal dependencies
*/
import { Sidebar } from '../sidebar';
+import { Tabs } from '../tabs';
type BlockEditorProps = {
product: Partial< Product >;
@@ -48,6 +57,8 @@ export function BlockEditor( {
settings: _settings,
product,
}: BlockEditorProps ) {
+ const [ selectedTab, setSelectedTab ] = useState< string | null >( null );
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore __experimentalTearDownEditor is not yet included in types package.
const { setupEditor, __experimentalTearDownEditor } =
@@ -102,29 +113,32 @@ export function BlockEditor( {
return (
-
-
-
-
-
-
- { /* eslint-disable-next-line @typescript-eslint/ban-ts-comment */ }
- { /* @ts-ignore No types for this exist yet. */ }
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+ { /* eslint-disable-next-line @typescript-eslint/ban-ts-comment */ }
+ { /* @ts-ignore No types for this exist yet. */ }
+
+
+
+
+
+
+
+
+
+
+
);
}
diff --git a/packages/js/product-editor/src/components/editor/init-blocks.ts b/packages/js/product-editor/src/components/editor/init-blocks.ts
index c3bdb46bd9f..c91b20202e3 100644
--- a/packages/js/product-editor/src/components/editor/init-blocks.ts
+++ b/packages/js/product-editor/src/components/editor/init-blocks.ts
@@ -3,8 +3,10 @@
*/
import { init as initName } from '../details-name-block';
import { init as initSection } from '../section';
+import { init as initTab } from '../tab';
export const initBlocks = () => {
initName();
initSection();
+ initTab();
};
diff --git a/packages/js/product-editor/src/components/section/block.json b/packages/js/product-editor/src/components/section/block.json
index b11a9f91fd3..e1d8a35c111 100644
--- a/packages/js/product-editor/src/components/section/block.json
+++ b/packages/js/product-editor/src/components/section/block.json
@@ -18,7 +18,7 @@
"supports": {
"align": false,
"html": false,
- "multiple": false,
+ "multiple": true,
"reusable": false,
"inserter": false,
"lock": false
diff --git a/packages/js/product-editor/src/components/tab/block.json b/packages/js/product-editor/src/components/tab/block.json
new file mode 100644
index 00000000000..f9edf354676
--- /dev/null
+++ b/packages/js/product-editor/src/components/tab/block.json
@@ -0,0 +1,27 @@
+{
+ "$schema": "https://schemas.wp.org/trunk/block.json",
+ "apiVersion": 2,
+ "name": "woocommerce/product-tab",
+ "title": "Product tab",
+ "category": "woocommerce",
+ "description": "The product tab.",
+ "keywords": [ "products", "tab", "group" ],
+ "textdomain": "default",
+ "attributes": {
+ "id": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string"
+ }
+ },
+ "supports": {
+ "align": false,
+ "html": false,
+ "multiple": true,
+ "reusable": false,
+ "inserter": false,
+ "lock": false
+ },
+ "usesContext": [ "selectedTab" ]
+}
diff --git a/packages/js/product-editor/src/components/tab/edit.tsx b/packages/js/product-editor/src/components/tab/edit.tsx
new file mode 100644
index 00000000000..0a08b67a8de
--- /dev/null
+++ b/packages/js/product-editor/src/components/tab/edit.tsx
@@ -0,0 +1,44 @@
+/**
+ * External dependencies
+ */
+import classnames from 'classnames';
+import { createElement } from '@wordpress/element';
+import { InnerBlocks, useBlockProps } from '@wordpress/block-editor';
+import type { BlockAttributes } from '@wordpress/blocks';
+
+/**
+ * Internal dependencies
+ */
+import { TabButton } from './tab-button';
+
+export function Edit( {
+ attributes,
+ context,
+}: {
+ attributes: BlockAttributes;
+ context?: {
+ selectedTab?: string;
+ };
+} ) {
+ const blockProps = useBlockProps();
+ const { id, title } = attributes;
+ const isSelected = context?.selectedTab === id;
+
+ const classes = classnames( 'wp-block-woocommerce-product-tab__content', {
+ 'is-selected': isSelected,
+ } );
+
+ return (
+
+ );
+}
diff --git a/packages/js/product-editor/src/components/tab/index.ts b/packages/js/product-editor/src/components/tab/index.ts
new file mode 100644
index 00000000000..15144b2b28d
--- /dev/null
+++ b/packages/js/product-editor/src/components/tab/index.ts
@@ -0,0 +1,17 @@
+/**
+ * Internal dependencies
+ */
+import initBlock from '../../utils/init-block';
+import metadata from './block.json';
+import { Edit } from './edit';
+
+const { name } = metadata;
+
+export { metadata, name };
+
+export const settings = {
+ example: {},
+ edit: Edit,
+};
+
+export const init = () => initBlock( { name, metadata, settings } );
diff --git a/packages/js/product-editor/src/components/tab/style.scss b/packages/js/product-editor/src/components/tab/style.scss
new file mode 100644
index 00000000000..867fff9ee9a
--- /dev/null
+++ b/packages/js/product-editor/src/components/tab/style.scss
@@ -0,0 +1,5 @@
+.wp-block-woocommerce-product-tab__content {
+ &:not(.is-selected) {
+ display: none;
+ }
+}
\ No newline at end of file
diff --git a/packages/js/product-editor/src/components/tab/tab-button.tsx b/packages/js/product-editor/src/components/tab/tab-button.tsx
new file mode 100644
index 00000000000..dbc86e75251
--- /dev/null
+++ b/packages/js/product-editor/src/components/tab/tab-button.tsx
@@ -0,0 +1,44 @@
+/**
+ * External dependencies
+ */
+import { Button, Fill } from '@wordpress/components';
+import classnames from 'classnames';
+import { createElement } from '@wordpress/element';
+
+/**
+ * Internal dependencies
+ */
+import { TABS_SLOT_NAME } from '../tabs/constants';
+import { TabsFillProps } from '../tabs';
+
+export function TabButton( {
+ children,
+ className,
+ id,
+}: {
+ children: string | JSX.Element;
+ className?: string;
+ id: string;
+} ) {
+ const classes = classnames(
+ 'wp-block-woocommerce-product-tab__button',
+ className
+ );
+
+ return (
+
+ { ( fillProps: TabsFillProps ) => {
+ const { onClick } = fillProps;
+ return (
+
+ );
+ } }
+
+ );
+}
diff --git a/packages/js/product-editor/src/components/tabs/constants.ts b/packages/js/product-editor/src/components/tabs/constants.ts
new file mode 100644
index 00000000000..0b2fc863fb7
--- /dev/null
+++ b/packages/js/product-editor/src/components/tabs/constants.ts
@@ -0,0 +1 @@
+export const TABS_SLOT_NAME = 'woocommerce_product_tabs';
diff --git a/packages/js/product-editor/src/components/tabs/index.ts b/packages/js/product-editor/src/components/tabs/index.ts
new file mode 100644
index 00000000000..c2d1b4e91b2
--- /dev/null
+++ b/packages/js/product-editor/src/components/tabs/index.ts
@@ -0,0 +1 @@
+export * from './tabs';
diff --git a/packages/js/product-editor/src/components/tabs/style.scss b/packages/js/product-editor/src/components/tabs/style.scss
new file mode 100644
index 00000000000..c12769d0562
--- /dev/null
+++ b/packages/js/product-editor/src/components/tabs/style.scss
@@ -0,0 +1,17 @@
+.woocommerce-product-tabs {
+ display: flex;
+ justify-content: center;
+
+ .components-button {
+ padding: $gap-smaller 0 20px 0;
+ margin-left: $gap;
+ margin-right: $gap;
+ border-bottom: 3.5px solid transparent;
+ border-radius: 0;
+ height: auto;
+
+ &.is-selected {
+ border-color: var(--wp-admin-theme-color);
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/js/product-editor/src/components/tabs/tabs.tsx b/packages/js/product-editor/src/components/tabs/tabs.tsx
new file mode 100644
index 00000000000..c4c0c0bfdcd
--- /dev/null
+++ b/packages/js/product-editor/src/components/tabs/tabs.tsx
@@ -0,0 +1,84 @@
+/**
+ * External dependencies
+ */
+import {
+ createElement,
+ Fragment,
+ useEffect,
+ useState,
+} from '@wordpress/element';
+import { ReactElement } from 'react';
+import { Slot } from '@wordpress/components';
+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+// @ts-ignore No types for this exist yet.
+// eslint-disable-next-line @woocommerce/dependency-group
+import { navigateTo, getNewPath, getQuery } from '@woocommerce/navigation';
+
+/**
+ * Internal dependencies
+ */
+import { TABS_SLOT_NAME } from './constants';
+
+type TabsProps = {
+ onChange: ( tabId: string | null ) => void;
+};
+
+export type TabsFillProps = {
+ onClick: ( tabId: string ) => void;
+};
+
+export function Tabs( { onChange = () => {} }: TabsProps ) {
+ const [ selected, setSelected ] = useState< string | null >( null );
+ const query = getQuery() as Record< string, string >;
+
+ function onClick( tabId: string ) {
+ window.document.documentElement.scrollTop = 0;
+ navigateTo( {
+ url: getNewPath( { tab: tabId } ),
+ } );
+ }
+
+ useEffect( () => {
+ onChange( selected );
+ }, [ selected ] );
+
+ useEffect( () => {
+ if ( query.tab ) {
+ setSelected( query.tab );
+ }
+ }, [ query.tab ] );
+
+ function maybeSetSelected( fills: readonly ( readonly ReactElement[] )[] ) {
+ if ( selected ) {
+ return;
+ }
+
+ for ( let i = 0; i < fills.length; i++ ) {
+ if ( fills[ i ][ 0 ].props.disabled ) {
+ continue;
+ }
+ // Remove the `.$` prefix on keys. E.g., .$key => key
+ const tabId = fills[ i ][ 0 ].key?.toString().slice( 2 ) || null;
+ setSelected( tabId );
+ return;
+ }
+ }
+
+ return (
+
+
+ { ( fills ) => {
+ maybeSetSelected( fills );
+ return <>{ fills }>;
+ } }
+
+
+ );
+}
diff --git a/packages/js/product-editor/src/style.scss b/packages/js/product-editor/src/style.scss
index 6ec6591af40..99f2ce949ee 100644
--- a/packages/js/product-editor/src/style.scss
+++ b/packages/js/product-editor/src/style.scss
@@ -6,3 +6,5 @@
@import 'components/header/style.scss';
@import 'components/block-editor/style.scss';
@import 'components/section/style.scss';
+@import 'components/tab/style.scss';
+@import 'components/tabs/style.scss';
diff --git a/packages/js/product-editor/src/utils/init-block.ts b/packages/js/product-editor/src/utils/init-block.ts
index 55a8276f7b5..3e290a911eb 100644
--- a/packages/js/product-editor/src/utils/init-block.ts
+++ b/packages/js/product-editor/src/utils/init-block.ts
@@ -1,12 +1,25 @@
/**
* External dependencies
*/
-import { BlockConfiguration, registerBlockType } from '@wordpress/blocks';
+import {
+ BlockConfiguration,
+ BlockEditProps,
+ registerBlockType,
+} from '@wordpress/blocks';
+import { ComponentType } from 'react';
type BlockRepresentation = {
name: string;
metadata: BlockConfiguration;
- settings: Partial< BlockConfiguration >;
+ settings: Partial< Omit< BlockConfiguration, 'edit' > > & {
+ readonly edit?:
+ | ComponentType<
+ BlockEditProps< object > & {
+ context?: Record< string, unknown >;
+ }
+ >
+ | undefined;
+ };
};
/**
diff --git a/plugins/woocommerce/changelog/add-37096 b/plugins/woocommerce/changelog/add-37096
new file mode 100644
index 00000000000..9644a87c81e
--- /dev/null
+++ b/plugins/woocommerce/changelog/add-37096
@@ -0,0 +1,4 @@
+Significance: minor
+Type: update
+
+Add tabs and sections placeholders in product blocks template
diff --git a/plugins/woocommerce/includes/class-wc-post-types.php b/plugins/woocommerce/includes/class-wc-post-types.php
index 2d39ae28231..326102f9788 100644
--- a/plugins/woocommerce/includes/class-wc-post-types.php
+++ b/plugins/woocommerce/includes/class-wc-post-types.php
@@ -368,9 +368,61 @@ class WC_Post_Types {
'rest_namespace' => 'wp/v3',
'template' => array(
array(
- 'woocommerce/product-name',
+ 'woocommerce/product-tab',
array(
- 'name' => 'Product name',
+ 'id' => 'general',
+ 'title' => __( 'General', 'woocommerce' ),
+ ),
+ array(
+ array(
+ 'woocommerce/product-section',
+ array(
+ 'title' => __( 'Basic details', 'woocommerce' ),
+ 'description' => __( 'This info will be displayed on the product page, category pages, social media, and search results.', 'woocommerce' ),
+ ),
+ array(
+ array(
+ 'woocommerce/product-name',
+ array(
+ 'name' => 'Product name',
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ array(
+ 'woocommerce/product-tab',
+ array(
+ 'id' => 'pricing',
+ 'title' => __( 'Pricing', 'woocommerce' ),
+ ),
+ ),
+ array(
+ 'woocommerce/product-tab',
+ array(
+ 'id' => 'inventory',
+ 'title' => __( 'Inventory', 'woocommerce' ),
+ ),
+ ),
+ array(
+ 'woocommerce/product-tab',
+ array(
+ 'id' => 'shipping',
+ 'title' => __( 'Shipping', 'woocommerce' ),
+ ),
+ array(
+ array(
+ 'woocommerce/product-section',
+ array(
+ 'title' => __( 'Shipping section', 'woocommerce' ),
+ ),
+ array(
+ array(
+ 'core/image',
+ ),
+ ),
+ ),
),
),
),
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 46af508412b..f0ab5a88dec 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1371,6 +1371,7 @@ importers:
'@woocommerce/data': workspace:^4.1.0
'@woocommerce/eslint-plugin': workspace:*
'@woocommerce/internal-style-build': workspace:*
+ '@woocommerce/navigation': workspace:^8.1.0
'@woocommerce/number': workspace:*
'@woocommerce/tracks': workspace:^1.3.0
'@wordpress/block-editor': ^9.8.0
@@ -1415,6 +1416,7 @@ importers:
'@woocommerce/components': link:../components
'@woocommerce/currency': link:../currency
'@woocommerce/data': link:../data
+ '@woocommerce/navigation': link:../navigation
'@woocommerce/number': link:../number
'@woocommerce/tracks': link:../tracks
'@wordpress/block-editor': 9.8.0_vcke6catv4iqpjdw24uwvlzyyi
@@ -15314,7 +15316,7 @@ packages:
'@wordpress/rich-text': 5.17.0_react@17.0.2
'@wordpress/shortcode': 3.28.0
'@wordpress/style-engine': 1.2.0
- '@wordpress/token-list': 2.19.0
+ '@wordpress/token-list': 2.28.0
'@wordpress/url': 3.29.0
'@wordpress/warning': 2.28.0
'@wordpress/wordcount': 3.28.0
@@ -33587,7 +33589,7 @@ packages:
postcss: 8.4.12
schema-utils: 3.1.1
semver: 7.3.8
- webpack: 5.70.0
+ webpack: 5.70.0_webpack-cli@3.3.12
/postcss-loader/6.2.1_wn4p5kzkgq2ohl66pfawxjf2x4:
resolution: {integrity: sha512-WbbYpmAaKcux/P66bZ40bpWsBucjx/TTgVVzRZ9yUO8yQfVBlameJ0ZGVaPfH64hNSBh63a+ICP5nqOpBA0w+Q==}
@@ -35003,7 +35005,7 @@ packages:
is-touch-device: 1.0.1
lodash: 4.17.21
moment: 2.29.4
- object.assign: 4.1.2
+ object.assign: 4.1.4
object.values: 1.1.5
prop-types: 15.8.1
raf: 3.4.1