Adding WooProductFieldItem slotfill (#36315)

This commit is contained in:
Joel Thiessen 2023-01-10 10:13:02 -08:00 committed by GitHub
parent c6e1fb767e
commit f429b9444c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 161 additions and 257 deletions

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Adding WooProductFieldItem slotfill.

View File

@ -84,3 +84,5 @@ export { DynamicForm } from './dynamic-form';
export { default as TourKit } from './tour-kit';
export * as TourKitTypes from './tour-kit/types';
export { CollapsibleContent } from './collapsible-content';
export { createOrderedChildren, sortFillsByOrder } from './utils';
export { WooProductFieldItem as __experimentalWooProductFieldItem } from './woo-product-field-item';

View File

@ -0,0 +1,43 @@
/**
* External dependencies
*/
import React, { isValidElement, Fragment } from 'react';
import { Slot, Fill } from '@wordpress/components';
import { cloneElement, createElement } from '@wordpress/element';
/**
* Ordered fill item.
*
* @param {Node} children - Node children.
* @param {number} order - Node order.
* @param {Array} props - Fill props.
* @return {Node} Node.
*/
function createOrderedChildren< T = Fill.Props >(
children: React.ReactNode,
order: number,
props: T
) {
if ( typeof children === 'function' ) {
return cloneElement( children( props ), { order } );
} else if ( isValidElement( children ) ) {
return cloneElement( children, { ...props, order } );
}
throw Error( 'Invalid children type' );
}
export { createOrderedChildren };
/**
* Sort fills by order for slot children.
*
* @param {Array} fills - slot's `Fill`s.
* @return {Node} Node.
*/
export const sortFillsByOrder: Slot.Props[ 'children' ] = ( fills ) => {
// Copy fills array here because its type is readonly array that doesn't have .sort method in Typescript definition.
const sortedFills = [ ...fills ].sort( ( a, b ) => {
return a[ 0 ].props.order - b[ 0 ].props.order;
} );
return <Fragment>{ sortedFills }</Fragment>;
};

View File

@ -0,0 +1,42 @@
# WooProductFieldItem Slot & Fill
A Slotfill component that will allow you to add a new field to a specific section in the product editor.
## Usage
```jsx
<WooProductFieldItem id={ key } section="details" order={ 2 } pluginId="test-plugin" >
{ () => {
return (
<TextControl
label="Name"
name={ `product-mvp-name` }
placeholder="e.g. 12 oz Coffee Mug"
value="Test Name"
onChange={ () => console.debug( 'Changed!' ) }
/>
);
} }
</WooProductFieldItem>
<WooProductFieldItem.Slot section="details" />
```
### WooProductFieldItem (fill)
This is the fill component. You must provide the `id` prop to identify your product field fill with a unique string. This component will accept a series of props:
| Prop | Type | Description |
| -------------| -------- | ------------------------------------------------------------------------------------------------------------------------ |
| `id` | String | A unique string to identify your fill. Used for configuiration management. |
| `section ` | String | The string used to identify the particular section where you want to render your field. |
| `pluginId` | String | A unique plugin ID to identify the plugin/extension that this fill is associated with. |
| `order` | Number | (optional) This number will dictate the order that the fields rendered by a Slot will be appear. |
### WooProductFieldItem.Slot (slot)
This is the slot component, and will not be used as frequently. It must also receive the required `location` prop that will be identical to the fill `location`.
| Name | Type | Description |
| ----------- | ------ | ---------------------------------------------------------------------------------------------------- |
| `section` | String | Unique to the section that the Slot appears, and must be the same as the one provided to any fills. |

View File

@ -0,0 +1 @@
export * from './woo-product-field-item';

View File

@ -0,0 +1,58 @@
/**
* External dependencies
*/
import React from 'react';
import { Slot, Fill } from '@wordpress/components';
import { createElement, Children } from '@wordpress/element';
/**
* Internal dependencies
*/
import { createOrderedChildren, sortFillsByOrder } from '../utils';
type WooProductFieldItemProps = {
id: string;
section: string;
pluginId: string;
order?: number;
};
type WooProductFieldSlotProps = {
section: string;
};
export const WooProductFieldItem: React.FC< WooProductFieldItemProps > & {
Slot: React.FC< Slot.Props & WooProductFieldSlotProps >;
} = ( { children, order = 1, section } ) => (
<Fill name={ `woocommerce_product_field_${ section }` }>
{ ( fillProps: Fill.Props ) => {
return createOrderedChildren< Fill.Props >(
children,
order,
fillProps
);
} }
</Fill>
);
WooProductFieldItem.Slot = ( { fillProps, section } ) => (
<Slot
name={ `woocommerce_product_field_${ section }` }
fillProps={ fillProps }
>
{ ( fills ) => {
if ( ! sortFillsByOrder ) {
return null;
}
return Children.map(
sortFillsByOrder( fills )?.props.children,
( child ) => (
<div className="woocommerce-product-form__field">
{ child }
</div>
)
);
} }
</Slot>
);

View File

@ -1,31 +0,0 @@
/**
* Internal dependencies
*/
import { WooProductFieldItem } from './woo-product-field-item';
type ProductFieldLayoutProps = {
fieldName: string;
categoryName: string;
};
export const ProductFieldLayout: React.FC< ProductFieldLayoutProps > = ( {
fieldName,
categoryName,
children,
} ) => {
return (
<div className="product-field-layout">
<WooProductFieldItem.Slot
fieldName={ fieldName }
categoryName={ categoryName }
location="before"
/>
{ children }
<WooProductFieldItem.Slot
fieldName={ fieldName }
categoryName={ categoryName }
location="after"
/>
</div>
);
};

View File

@ -22,7 +22,7 @@
}
}
.woocommerce-product-form__field {
.woocommerce-product-form__field:not(:first-child) {
margin-top: $gap-large;
> .components-base-control {

View File

@ -8,7 +8,6 @@ import { FormSection } from '@woocommerce/components';
* Internal dependencies
*/
import './product-section-layout.scss';
import { ProductFieldLayout } from './product-field-layout';
type ProductSectionLayoutProps = {
title: string;
@ -31,12 +30,7 @@ export const ProductSectionLayout: React.FC< ProductSectionLayoutProps > = ( {
{ Children.map( children, ( child ) => {
if ( isValidElement( child ) && child.props.onChange ) {
return (
<ProductFieldLayout
fieldName={ child.props.name }
categoryName={ title }
>
{ child }
</ProductFieldLayout>
<div className="product-field-layout">{ child }</div>
);
}
return child;

View File

@ -1,115 +0,0 @@
/**
* External dependencies
*/
import { render } from '@testing-library/react';
import { SlotFillProvider } from '@wordpress/components';
/**
* Internal dependencies
*/
import { ProductFieldLayout } from '../product-field-layout';
import { WooProductFieldItem } from '../woo-product-field-item';
describe( 'ProductFieldLayout', () => {
beforeEach( () => {
jest.clearAllMocks();
} );
it( 'should allow adding extra fields before the field using slot fill', () => {
const { queryByText } = render(
<SlotFillProvider>
<ProductFieldLayout
fieldName="Name"
categoryName="Product Details"
>
<div>Name field</div>
</ProductFieldLayout>
<div>
<WooProductFieldItem
fieldName="Name"
categoryName="Product Details"
location="before"
>
<div>New field</div>
</WooProductFieldItem>
</div>
</SlotFillProvider>
);
expect( queryByText( 'New field' ) ).toBeInTheDocument();
expect( queryByText( 'New field' )?.nextSibling?.textContent ).toEqual(
'Name field'
);
} );
it( 'should allow adding extra fields after the field using slot fill', () => {
const { queryByText } = render(
<SlotFillProvider>
<ProductFieldLayout
fieldName="Name"
categoryName="Product Details"
>
<div>Name field</div>
</ProductFieldLayout>
<div>
<WooProductFieldItem
fieldName="Name"
categoryName="Product Details"
location="after"
>
<div>New field</div>
</WooProductFieldItem>
</div>
</SlotFillProvider>
);
expect( queryByText( 'New field' ) ).toBeInTheDocument();
expect( queryByText( 'Name field' )?.nextSibling?.textContent ).toEqual(
'New field'
);
} );
it( 'should not render new slot fills when field name does not match', () => {
const { queryByText } = render(
<SlotFillProvider>
<ProductFieldLayout
fieldName="Name"
categoryName="Product Details"
>
<div>Name field</div>
</ProductFieldLayout>
<div>
<WooProductFieldItem
fieldName="Description"
categoryName="Product Details"
location="after"
>
<div>New field</div>
</WooProductFieldItem>
</div>
</SlotFillProvider>
);
expect( queryByText( 'New field' ) ).not.toBeInTheDocument();
} );
it( 'should not render new slot fills when category name does not match', () => {
const { queryByText } = render(
<SlotFillProvider>
<ProductFieldLayout
fieldName="Name"
categoryName="Product Details"
>
<div>Name field</div>
</ProductFieldLayout>
<div>
<WooProductFieldItem
fieldName="Name"
categoryName="Images"
location="after"
>
<div>New field</div>
</WooProductFieldItem>
</div>
</SlotFillProvider>
);
expect( queryByText( 'New field' ) ).not.toBeInTheDocument();
} );
} );

View File

@ -8,28 +8,10 @@ import { render } from '@testing-library/react';
*/
import { ProductSectionLayout } from '../product-section-layout';
jest.mock( '../product-field-layout', () => {
const productFieldLayoutMock: React.FC< {
fieldName: string;
categoryName: string;
} > = ( { children, fieldName, categoryName } ) => {
return (
<div className="product-field-layout-mock">
<span>fieldName: { fieldName }</span>
<span>categoryName: { categoryName }</span>
{ children }
</div>
);
};
return {
ProductFieldLayout: productFieldLayoutMock,
};
} );
const SampleInputField: React.FC< { name: string; onChange: () => void } > = ( {
name,
} ) => {
return <div>smaple-input-field-{ name }</div>;
return <div>sample-input-field-{ name }</div>;
};
describe( 'ProductSectionLayout', () => {
@ -59,12 +41,9 @@ describe( 'ProductSectionLayout', () => {
</ProductSectionLayout>
);
expect( queryByText( 'fieldName: name' ) ).toBeInTheDocument();
expect( queryAllByText( 'categoryName: Title' ).length ).toEqual( 2 );
expect( queryByText( 'smaple-input-field-name' ) ).toBeInTheDocument();
expect( queryByText( 'sample-input-field-name' ) ).toBeInTheDocument();
expect(
queryByText( 'smaple-input-field-description' )
queryByText( 'sample-input-field-description' )
).toBeInTheDocument();
} );

View File

@ -1,79 +0,0 @@
/**
* External dependencies
*/
import React from 'react';
import { Slot, Fill } from '@wordpress/components';
import { snakeCase } from 'lodash';
/**
* Internal dependencies
*/
import { createOrderedChildren, sortFillsByOrder } from '~/utils';
// TODO: move this to a published JS package once ready.
/**
* Create a Fill for extensions to add items to the Product edit page.
*
* @slotFill WooProductFieldItem
* @scope woocommerce-admin
* @example
* const MyProductDetailsFieldItem = () => (
* <WooProductFieldItem fieldName="name" categoryName="Product details" location="after">My header item</WooProductFieldItem>
* );
*
* registerPlugin( 'my-extension', {
* render: MyProductDetailsFieldItem,
* scope: 'woocommerce-admin',
* } );
* @param {Object} param0
* @param {Array} param0.children - Node children.
* @param {string} param0.fieldName - Field name.
* @param {string} param0.categoryName - Category name.
* @param {number} param0.order - Order of Fill component.
* @param {string} param0.location - Location before or after.
*/
export const WooProductFieldItem: React.FC< {
fieldName: string;
categoryName: string;
order?: number;
location: 'before' | 'after';
} > & {
Slot: React.FC<
Slot.Props & {
fieldName: string;
categoryName: string;
location: 'before' | 'after';
}
>;
} = ( { children, fieldName, categoryName, location, order = 1 } ) => {
const categoryKey = snakeCase( categoryName );
const fieldKey = snakeCase( fieldName );
return (
<Fill
name={ `woocommerce_product_${ categoryKey }_${ fieldKey }_${ location }` }
>
{ ( fillProps: Fill.Props ) => {
return createOrderedChildren( children, order, fillProps );
} }
</Fill>
);
};
WooProductFieldItem.Slot = ( {
fillProps,
fieldName,
categoryName,
location,
} ) => {
const categoryKey = snakeCase( categoryName );
const fieldKey = snakeCase( fieldName );
return (
<Slot
name={ `woocommerce_product_${ categoryKey }_${ fieldKey }_${ location }` }
fillProps={ fillProps }
>
{ sortFillsByOrder }
</Slot>
);
};

View File

@ -17,6 +17,7 @@ import {
useFormContext,
__experimentalRichTextEditor as RichTextEditor,
__experimentalTooltip as Tooltip,
__experimentalWooProductFieldItem as WooProductFieldItem,
} from '@woocommerce/components';
import interpolateComponents from '@automattic/interpolate-components';
import {
@ -241,6 +242,7 @@ export const ProductDetailsSection: React.FC = () => {
'woocommerce'
) }
/>
<WooProductFieldItem.Slot section="details" />
</CardBody>
</Card>
</ProductSectionLayout>

View File

@ -0,0 +1,4 @@
Significance: minor
Type: update
Adding WooProductFieldItem slot to product details section.