Experimental SlotContext for managing slot fill interactions (#36333)

* Adding Slotcontext component and adding support to product slot fill components

* Passing inject props correctly to non-function components.
This commit is contained in:
Joel Thiessen 2023-01-19 01:52:45 -08:00 committed by GitHub
parent fc1745b03b
commit 2fae3537a7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 235 additions and 92 deletions

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Adding experimental component SlotContext

View File

@ -92,3 +92,9 @@ export {
ProductFieldSection as __experimentalProductFieldSection,
} from './product-section-layout';
export * from './product-fields';
export {
SlotContextProvider,
useSlotContext,
SlotContextType,
SlotContextHelpersType,
} from './slot-context';

View File

@ -0,0 +1 @@
export * from './slot-context';

View File

@ -0,0 +1,104 @@
/**
* External dependencies
*/
import {
createElement,
createContext,
useContext,
useCallback,
useReducer,
} from '@wordpress/element';
type FillConfigType = {
visible: boolean;
};
type FillType = Record< string, FillConfigType >;
type FillCollection = readonly ( readonly JSX.Element[] )[];
export type SlotContextHelpersType = {
hideFill: ( id: string ) => void;
showFill: ( id: string ) => void;
getFills: () => FillType;
};
export type SlotContextType = {
fills: FillType;
getFillHelpers: () => SlotContextHelpersType;
registerFill: ( id: string ) => void;
filterRegisteredFills: ( fillsArrays: FillCollection ) => FillCollection;
};
const SlotContext = createContext< SlotContextType | undefined >( undefined );
export const SlotContextProvider: React.FC = ( { children } ) => {
const [ fills, updateFills ] = useReducer(
( data: FillType, updates: FillType ) => ( { ...data, ...updates } ),
{}
);
const updateFillConfig = (
id: string,
update: Partial< FillConfigType >
) => {
if ( ! fills[ id ] ) {
throw new Error( `No fill found with ID: ${ id }` );
}
updateFills( { [ id ]: { ...fills[ id ], ...update } } );
};
const registerFill = useCallback(
( id: string ) => {
if ( fills[ id ] ) {
return;
}
updateFills( { [ id ]: { visible: true } } );
},
[ fills ]
);
const hideFill = useCallback(
( id: string ) => updateFillConfig( id, { visible: false } ),
[ fills ]
);
const showFill = useCallback(
( id: string ) => updateFillConfig( id, { visible: true } ),
[ fills ]
);
const getFills = useCallback( () => ( { ...fills } ), [ fills ] );
return (
<SlotContext.Provider
value={ {
registerFill,
getFillHelpers() {
return { hideFill, showFill, getFills };
},
filterRegisteredFills( fillsArrays: FillCollection ) {
return fillsArrays.filter(
( arr ) =>
fills[ arr[ 0 ].props._id ]?.visible !== false
);
},
fills,
} }
>
{ children }
</SlotContext.Provider>
);
};
export const useSlotContext = () => {
const slotContext = useContext( SlotContext );
if ( slotContext === undefined ) {
throw new Error(
'useSlotContext must be used within a SlotContextProvider'
);
}
return slotContext;
};

View File

@ -1,7 +1,7 @@
/**
* External dependencies
*/
import React, { isValidElement, Fragment } from 'react';
import { isValidElement, Fragment } from 'react';
import { Slot, Fill } from '@wordpress/components';
import { cloneElement, createElement } from '@wordpress/element';
@ -13,15 +13,16 @@ import { cloneElement, createElement } from '@wordpress/element';
* @param {Array} props - Fill props.
* @return {Node} Node.
*/
function createOrderedChildren< T = Fill.Props >(
function createOrderedChildren< T = Fill.Props, S = Record< string, unknown > >(
children: React.ReactNode,
order: number,
props: T
props: T,
injectProps?: S
) {
if ( typeof children === 'function' ) {
return cloneElement( children( props ), { order } );
return cloneElement( children( props ), { order, ...injectProps } );
} else if ( isValidElement( children ) ) {
return cloneElement( children, { ...props, order } );
return cloneElement( children, { ...props, order, ...injectProps } );
}
throw Error( 'Invalid children type' );
}

View File

@ -9,6 +9,7 @@ import { createElement, Children } from '@wordpress/element';
* Internal dependencies
*/
import { createOrderedChildren, sortFillsByOrder } from '../utils';
import { useSlotContext, SlotContextHelpersType } from '../slot-context';
type WooProductFieldItemProps = {
id: string;
@ -23,36 +24,55 @@ type WooProductFieldSlotProps = {
export const WooProductFieldItem: React.FC< WooProductFieldItemProps > & {
Slot: React.FC< Slot.Props & WooProductFieldSlotProps >;
} = ( { children, order = 20, section } ) => (
<Fill name={ `woocommerce_product_field_${ section }` }>
{ ( fillProps: Fill.Props ) => {
return createOrderedChildren< Fill.Props >(
children,
order,
fillProps
);
} }
</Fill>
);
} = ( { children, order = 20, section, id } ) => {
const { registerFill, getFillHelpers } = useSlotContext();
WooProductFieldItem.Slot = ( { fillProps, section } ) => (
<Slot
name={ `woocommerce_product_field_${ section }` }
fillProps={ fillProps }
>
{ ( fills ) => {
if ( ! sortFillsByOrder ) {
return null;
}
registerFill( id );
return Children.map(
sortFillsByOrder( fills )?.props.children,
( child ) => (
<div className="woocommerce-product-form__field">
{ child }
</div>
)
);
} }
</Slot>
);
return (
<Fill name={ `woocommerce_product_field_${ section }` }>
{ ( fillProps: Fill.Props ) => {
return createOrderedChildren<
Fill.Props & SlotContextHelpersType,
{ _id: string }
>(
children,
order,
{
...fillProps,
...getFillHelpers(),
},
{ _id: id }
);
} }
</Fill>
);
};
WooProductFieldItem.Slot = ( { fillProps, section } ) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const { filterRegisteredFills } = useSlotContext();
return (
<Slot
name={ `woocommerce_product_field_${ section }` }
fillProps={ fillProps }
>
{ ( fills ) => {
if ( ! sortFillsByOrder ) {
return null;
}
return Children.map(
sortFillsByOrder( filterRegisteredFills( fills ) )?.props
.children,
( child ) => (
<div className="woocommerce-product-form__field">
{ child }
</div>
)
);
} }
</Slot>
);
};

View File

@ -85,7 +85,5 @@ const DetailsSection = () => (
registerPlugin( 'wc-admin-product-editor-details-section', {
// @ts-expect-error 'scope' does exist. @types/wordpress__plugins is outdated.
scope: 'woocommerce-product-editor',
render: () => {
return <DetailsSection />;
},
render: () => <DetailsSection />,
} );

View File

@ -5,6 +5,7 @@ import {
Form,
FormRef,
__experimentalWooProductSectionItem as WooProductSectionItem,
SlotContextProvider,
} from '@woocommerce/components';
import { PartialProduct, Product } from '@woocommerce/data';
import { PluginArea } from '@wordpress/plugins';
@ -31,60 +32,64 @@ export const ProductForm: React.FC< {
formRef?: Ref< FormRef< Partial< Product > > >;
} > = ( { product, formRef } ) => {
return (
<Form< Partial< Product > >
initialValues={
product || {
reviews_allowed: true,
name: '',
sku: '',
stock_quantity: 0,
stock_status: 'instock',
<SlotContextProvider>
<Form< Partial< Product > >
initialValues={
product || {
reviews_allowed: true,
name: '',
sku: '',
stock_quantity: 0,
stock_status: 'instock',
}
}
}
ref={ formRef }
errors={ {} }
validate={ validate }
>
<ProductFormHeader />
<ProductFormLayout>
<ProductFormTab name="general" title="General">
<WooProductSectionItem.Slot location="tab/general" />
<ImagesSection />
<AttributesSection />
</ProductFormTab>
<ProductFormTab
name="pricing"
title="Pricing"
disabled={ !! product?.variations?.length }
>
<PricingSection />
</ProductFormTab>
<ProductFormTab
name="inventory"
title="Inventory"
disabled={ !! product?.variations?.length }
>
<ProductInventorySection />
</ProductFormTab>
<ProductFormTab
name="shipping"
title="Shipping"
disabled={ !! product?.variations?.length }
>
<ProductShippingSection product={ product } />
</ProductFormTab>
{ window.wcAdminFeatures[ 'product-variation-management' ] ? (
<ProductFormTab name="options" title="Options">
<OptionsSection />
<ProductVariationsSection />
ref={ formRef }
errors={ {} }
validate={ validate }
>
<ProductFormHeader />
<ProductFormLayout>
<ProductFormTab name="general" title="General">
<WooProductSectionItem.Slot location="tab/general" />
<ImagesSection />
<AttributesSection />
</ProductFormTab>
) : (
<></>
) }
</ProductFormLayout>
<ProductFormFooter />
{ /* @ts-expect-error 'scope' does exist. @types/wordpress__plugins is outdated. */ }
<PluginArea scope="woocommerce-product-editor" />
</Form>
<ProductFormTab
name="pricing"
title="Pricing"
disabled={ !! product?.variations?.length }
>
<PricingSection />
</ProductFormTab>
<ProductFormTab
name="inventory"
title="Inventory"
disabled={ !! product?.variations?.length }
>
<ProductInventorySection />
</ProductFormTab>
<ProductFormTab
name="shipping"
title="Shipping"
disabled={ !! product?.variations?.length }
>
<ProductShippingSection product={ product } />
</ProductFormTab>
{ window.wcAdminFeatures[
'product-variation-management'
] ? (
<ProductFormTab name="options" title="Options">
<OptionsSection />
<ProductVariationsSection />
</ProductFormTab>
) : (
<></>
) }
</ProductFormLayout>
<ProductFormFooter />
{ /* @ts-expect-error 'scope' does exist. @types/wordpress__plugins is outdated. */ }
<PluginArea scope="woocommerce-product-editor" />
</Form>
</SlotContextProvider>
);
};

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Trying experimental slot context with product editor fills.