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