Add tab blocks to the blocks product editor (#37174)
* Add initial tab block and tabs render * Add tabs styling * Set initially selected tab on render * Set initial tab based on query param * Add template placeholder with tabs * Add changelog entry * Allow multiple tab and section blocks * Add changelog entry for wc * Fix up arrow alignment * Fix up block configuration type * Add missing navigation dependency to product editor
This commit is contained in:
parent
b1a0d3177c
commit
31ec8d8352
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Add tabs block and tabs to product editor
|
|
@ -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",
|
||||
|
|
|
@ -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 (
|
||||
<div className="woocommerce-product-block-editor">
|
||||
<BlockEditorProvider
|
||||
value={ blocks }
|
||||
onInput={ onInput }
|
||||
onChange={ onChange }
|
||||
settings={ settings }
|
||||
>
|
||||
<BlockBreadcrumb />
|
||||
<Sidebar.InspectorFill>
|
||||
<BlockInspector />
|
||||
</Sidebar.InspectorFill>
|
||||
<div className="editor-styles-wrapper">
|
||||
{ /* eslint-disable-next-line @typescript-eslint/ban-ts-comment */ }
|
||||
{ /* @ts-ignore No types for this exist yet. */ }
|
||||
<BlockEditorKeyboardShortcuts.Register />
|
||||
<BlockTools>
|
||||
<WritingFlow>
|
||||
<ObserveTyping>
|
||||
<BlockList className="woocommerce-product-block-editor__block-list" />
|
||||
</ObserveTyping>
|
||||
</WritingFlow>
|
||||
</BlockTools>
|
||||
</div>
|
||||
</BlockEditorProvider>
|
||||
<BlockContextProvider value={ { selectedTab } }>
|
||||
<BlockEditorProvider
|
||||
value={ blocks }
|
||||
onInput={ onInput }
|
||||
onChange={ onChange }
|
||||
settings={ settings }
|
||||
>
|
||||
<Tabs onChange={ setSelectedTab } />
|
||||
<BlockBreadcrumb />
|
||||
<Sidebar.InspectorFill>
|
||||
<BlockInspector />
|
||||
</Sidebar.InspectorFill>
|
||||
<div className="editor-styles-wrapper">
|
||||
{ /* eslint-disable-next-line @typescript-eslint/ban-ts-comment */ }
|
||||
{ /* @ts-ignore No types for this exist yet. */ }
|
||||
<BlockEditorKeyboardShortcuts.Register />
|
||||
<BlockTools>
|
||||
<WritingFlow>
|
||||
<ObserveTyping>
|
||||
<BlockList className="woocommerce-product-block-editor__block-list" />
|
||||
</ObserveTyping>
|
||||
</WritingFlow>
|
||||
</BlockTools>
|
||||
</div>
|
||||
</BlockEditorProvider>
|
||||
</BlockContextProvider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
};
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
"supports": {
|
||||
"align": false,
|
||||
"html": false,
|
||||
"multiple": false,
|
||||
"multiple": true,
|
||||
"reusable": false,
|
||||
"inserter": false,
|
||||
"lock": false
|
||||
|
|
|
@ -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" ]
|
||||
}
|
|
@ -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 (
|
||||
<div { ...blockProps }>
|
||||
<TabButton
|
||||
id={ id }
|
||||
className={ isSelected ? 'is-selected' : undefined }
|
||||
>
|
||||
{ title }
|
||||
</TabButton>
|
||||
<div className={ classes }>
|
||||
<InnerBlocks templateLock="all" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -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 } );
|
|
@ -0,0 +1,5 @@
|
|||
.wp-block-woocommerce-product-tab__content {
|
||||
&:not(.is-selected) {
|
||||
display: none;
|
||||
}
|
||||
}
|
|
@ -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 (
|
||||
<Fill name={ TABS_SLOT_NAME }>
|
||||
{ ( fillProps: TabsFillProps ) => {
|
||||
const { onClick } = fillProps;
|
||||
return (
|
||||
<Button
|
||||
key={ id }
|
||||
className={ classes }
|
||||
onClick={ () => onClick( id ) }
|
||||
>
|
||||
{ children }
|
||||
</Button>
|
||||
);
|
||||
} }
|
||||
</Fill>
|
||||
);
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export const TABS_SLOT_NAME = 'woocommerce_product_tabs';
|
|
@ -0,0 +1 @@
|
|||
export * from './tabs';
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 (
|
||||
<div className="woocommerce-product-tabs">
|
||||
<Slot
|
||||
fillProps={
|
||||
{
|
||||
onClick,
|
||||
} as TabsFillProps
|
||||
}
|
||||
name={ TABS_SLOT_NAME }
|
||||
>
|
||||
{ ( fills ) => {
|
||||
maybeSetSelected( fills );
|
||||
return <>{ fills }</>;
|
||||
} }
|
||||
</Slot>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: update
|
||||
|
||||
Add tabs and sections placeholders in product blocks template
|
|
@ -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',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue