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:
parent
fc1745b03b
commit
2fae3537a7
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Adding experimental component SlotContext
|
|
@ -92,3 +92,9 @@ export {
|
|||
ProductFieldSection as __experimentalProductFieldSection,
|
||||
} from './product-section-layout';
|
||||
export * from './product-fields';
|
||||
export {
|
||||
SlotContextProvider,
|
||||
useSlotContext,
|
||||
SlotContextType,
|
||||
SlotContextHelpersType,
|
||||
} from './slot-context';
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export * from './slot-context';
|
|
@ -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;
|
||||
};
|
|
@ -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' );
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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 />,
|
||||
} );
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Trying experimental slot context with product editor fills.
|
Loading…
Reference in New Issue