diff --git a/packages/js/components/changelog/try-product-mvp-slotfill-experiments b/packages/js/components/changelog/try-product-mvp-slotfill-experiments new file mode 100644 index 00000000000..7df70dd246d --- /dev/null +++ b/packages/js/components/changelog/try-product-mvp-slotfill-experiments @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Adding experimental component SlotContext diff --git a/packages/js/components/src/index.ts b/packages/js/components/src/index.ts index 94562eb2dce..8cea1e13515 100644 --- a/packages/js/components/src/index.ts +++ b/packages/js/components/src/index.ts @@ -92,3 +92,9 @@ export { ProductFieldSection as __experimentalProductFieldSection, } from './product-section-layout'; export * from './product-fields'; +export { + SlotContextProvider, + useSlotContext, + SlotContextType, + SlotContextHelpersType, +} from './slot-context'; diff --git a/packages/js/components/src/slot-context/index.ts b/packages/js/components/src/slot-context/index.ts new file mode 100644 index 00000000000..86ac84d1fdf --- /dev/null +++ b/packages/js/components/src/slot-context/index.ts @@ -0,0 +1 @@ +export * from './slot-context'; diff --git a/packages/js/components/src/slot-context/slot-context.tsx b/packages/js/components/src/slot-context/slot-context.tsx new file mode 100644 index 00000000000..49b70ccb86a --- /dev/null +++ b/packages/js/components/src/slot-context/slot-context.tsx @@ -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 ( + + fills[ arr[ 0 ].props._id ]?.visible !== false + ); + }, + fills, + } } + > + { children } + + ); +}; + +export const useSlotContext = () => { + const slotContext = useContext( SlotContext ); + + if ( slotContext === undefined ) { + throw new Error( + 'useSlotContext must be used within a SlotContextProvider' + ); + } + + return slotContext; +}; diff --git a/packages/js/components/src/utils.tsx b/packages/js/components/src/utils.tsx index 682a1a0aded..34117864d5d 100644 --- a/packages/js/components/src/utils.tsx +++ b/packages/js/components/src/utils.tsx @@ -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' ); } diff --git a/packages/js/components/src/woo-product-field-item/woo-product-field-item.tsx b/packages/js/components/src/woo-product-field-item/woo-product-field-item.tsx index 3a62adf24df..2db6a28165d 100644 --- a/packages/js/components/src/woo-product-field-item/woo-product-field-item.tsx +++ b/packages/js/components/src/woo-product-field-item/woo-product-field-item.tsx @@ -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 } ) => ( - - { ( fillProps: Fill.Props ) => { - return createOrderedChildren< Fill.Props >( - children, - order, - fillProps - ); - } } - -); +} = ( { children, order = 20, section, id } ) => { + const { registerFill, getFillHelpers } = useSlotContext(); -WooProductFieldItem.Slot = ( { fillProps, section } ) => ( - - { ( fills ) => { - if ( ! sortFillsByOrder ) { - return null; - } + registerFill( id ); - return Children.map( - sortFillsByOrder( fills )?.props.children, - ( child ) => ( -
- { child } -
- ) - ); - } } -
-); + return ( + + { ( fillProps: Fill.Props ) => { + return createOrderedChildren< + Fill.Props & SlotContextHelpersType, + { _id: string } + >( + children, + order, + { + ...fillProps, + ...getFillHelpers(), + }, + { _id: id } + ); + } } + + ); +}; + +WooProductFieldItem.Slot = ( { fillProps, section } ) => { + // eslint-disable-next-line react-hooks/rules-of-hooks + const { filterRegisteredFills } = useSlotContext(); + + return ( + + { ( fills ) => { + if ( ! sortFillsByOrder ) { + return null; + } + + return Children.map( + sortFillsByOrder( filterRegisteredFills( fills ) )?.props + .children, + ( child ) => ( +
+ { child } +
+ ) + ); + } } +
+ ); +}; diff --git a/plugins/woocommerce-admin/client/products/fills/details-section/details-section-fills.tsx b/plugins/woocommerce-admin/client/products/fills/details-section/details-section-fills.tsx index 73646b5d61c..bf203331e30 100644 --- a/plugins/woocommerce-admin/client/products/fills/details-section/details-section-fills.tsx +++ b/plugins/woocommerce-admin/client/products/fills/details-section/details-section-fills.tsx @@ -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 ; - }, + render: () => , } ); diff --git a/plugins/woocommerce-admin/client/products/product-form.tsx b/plugins/woocommerce-admin/client/products/product-form.tsx index 60abf3f26b5..0f1a171fafb 100644 --- a/plugins/woocommerce-admin/client/products/product-form.tsx +++ b/plugins/woocommerce-admin/client/products/product-form.tsx @@ -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 ( - > - initialValues={ - product || { - reviews_allowed: true, - name: '', - sku: '', - stock_quantity: 0, - stock_status: 'instock', + + > + initialValues={ + product || { + reviews_allowed: true, + name: '', + sku: '', + stock_quantity: 0, + stock_status: 'instock', + } } - } - ref={ formRef } - errors={ {} } - validate={ validate } - > - - - - - - - - - - - - - - - - - { window.wcAdminFeatures[ 'product-variation-management' ] ? ( - - - + ref={ formRef } + errors={ {} } + validate={ validate } + > + + + + + + - ) : ( - <> - ) } - - - { /* @ts-expect-error 'scope' does exist. @types/wordpress__plugins is outdated. */ } - - + + + + + + + + + + { window.wcAdminFeatures[ + 'product-variation-management' + ] ? ( + + + + + ) : ( + <> + ) } + + + { /* @ts-expect-error 'scope' does exist. @types/wordpress__plugins is outdated. */ } + + + ); }; diff --git a/plugins/woocommerce/changelog/try-product-mvp-slotfill-experiments b/plugins/woocommerce/changelog/try-product-mvp-slotfill-experiments new file mode 100644 index 00000000000..735fdc59890 --- /dev/null +++ b/plugins/woocommerce/changelog/try-product-mvp-slotfill-experiments @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Trying experimental slot context with product editor fills.