diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 33677f10b58fc..aeb1de80a73c5 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -74,6 +74,7 @@ - `DropdownMenu` v2: add `GroupLabel` subcomponent ([#64854](https://github.com/WordPress/gutenberg/pull/64854)). - `DropdownMenuV2`: update animation ([#64868](https://github.com/WordPress/gutenberg/pull/64868)). - `Composite` V2: fix Storybook docgen ([#64682](https://github.com/WordPress/gutenberg/pull/64682)). +- `Composite` V2: add "With Slot Fill" example ([#65051](https://github.com/WordPress/gutenberg/pull/65051)). - `Composite` V2: accept store props on top-level component ([#64832](https://github.com/WordPress/gutenberg/pull/64832)). ## 28.6.0 (2024-08-21) diff --git a/packages/components/src/composite/stories/index.story.tsx b/packages/components/src/composite/stories/index.story.tsx index 80e3d85e3ce29..2fdec07925549 100644 --- a/packages/components/src/composite/stories/index.story.tsx +++ b/packages/components/src/composite/stories/index.story.tsx @@ -1,16 +1,18 @@ /** * External dependencies */ -import type { Meta, StoryFn } from '@storybook/react'; +import type { Meta, StoryFn, StoryContext } from '@storybook/react'; /** * WordPress dependencies */ import { isRTL } from '@wordpress/i18n'; +import { useContext, useMemo } from '@wordpress/element'; /** * Internal dependencies */ +import { createSlotFill, Provider as SlotFillProvider } from '../../slot-fill'; import { Composite } from '..'; import { useCompositeStore } from '../store'; import { UseCompositeStorePlaceholder, transform } from './utils'; @@ -204,3 +206,134 @@ Typeahead.parameters = { }, }, }; + +const ExampleSlotFill = createSlotFill( 'Example' ); + +const Slot = () => { + const compositeContext = useContext( Composite.Context ); + + // Forward the Slot's composite context to the Fill via fillProps, so that + // Composite components rendered inside the Fill can work as expected. + const fillProps = useMemo( + () => ( { + forwardedContext: [ + [ Composite.Context.Provider, { value: compositeContext } ], + ], + } ), + [ compositeContext ] + ); + + return ( + + ); +}; + +type ForwardedContextTuple< P = {} > = [ + React.ComponentType< React.PropsWithChildren< P > >, + P, +]; + +const Fill = ( { children }: { children: React.ReactNode } ) => { + const innerMarkup = <>{ children }; + + return ( + + { ( fillProps: { forwardedContext?: ForwardedContextTuple[] } ) => { + const { forwardedContext = [] } = fillProps; + + // Render all context providers forwarded by the Slot via fillProps. + return forwardedContext.reduce( + ( inner: JSX.Element, [ Provider, props ] ) => ( + { inner } + ), + innerMarkup + ); + } } + + ); +}; + +export const WithSlotFill: StoryFn< typeof UseCompositeStorePlaceholder > = ( + props +) => { + return ( + + + Item one (direct child) + + Item four (direct child) + + + + Item two (from slot fill) + Item three (from slot fill) + + + ); +}; +WithSlotFill.args = { + ...Default.args, +}; +WithSlotFill.parameters = { + docs: { + description: { + story: 'When rendering Composite components across a SlotFill, the Composite.Context should be manually forwarded from the Slot to the Fill component.', + }, + source: { + transform: ( code: string, storyContext: StoryContext ) => { + return `const ExampleSlotFill = createSlotFill( 'Example' ); + +const Slot = () => { + const compositeContext = useContext( Composite.Context ); + + // Forward the Slot's composite context to the Fill via fillProps, so that + // Composite components rendered inside the Fill can work as expected. + const fillProps = useMemo( + () => ( { + forwardedContext: [ + [ Composite.Context.Provider, { value: compositeContext } ], + ], + } ), + [ compositeContext ] + ); + + return ( + + ); +}; + +const Fill = ( { children } ) => { + const innerMarkup = <>{ children }; + + return ( + + { ( fillProps ) => { + const { forwardedContext = [] } = fillProps; + + // Render all context providers forwarded by the Slot via fillProps. + return forwardedContext.reduce( + ( inner, [ Provider, props ] ) => ( + { inner } + ), + innerMarkup + ); + } } + + ); +}; + +// In a separate component: + +${ transform( code, storyContext ) }`; + }, + }, + }, +};