From 8e373abf900cf4b0a0cec1b54d2f47d02b5444cc Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Tue, 29 Jun 2021 15:36:06 +1000 Subject: [PATCH 1/2] Add a reproducing story for #15400 --- .../stories/core/decorators.stories.js | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/examples/official-storybook/stories/core/decorators.stories.js b/examples/official-storybook/stories/core/decorators.stories.js index 7184460ac254..4dc07c8d41b3 100644 --- a/examples/official-storybook/stories/core/decorators.stories.js +++ b/examples/official-storybook/stories/core/decorators.stories.js @@ -35,3 +35,23 @@ All.decorators = [ ), ]; + +export const PropOverride = (args, { prop1, prop2 }) => ( +

+ Story prop:

{JSON.stringify({ prop1, prop2 })}
+

+); +PropOverride.decorators = [ + (Story) => ( + <> +

First local Decorator

+ + + ), + (Story) => ( + <> +

Second Local Decorator

+ + + ), +]; From 66b12b80fdba4a2d85f437f17718b137ff64eadc Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Tue, 29 Jun 2021 15:48:13 +1000 Subject: [PATCH 2/2] Keep updating the context store. As each decorator runs, we need to keep updating the "context store" to include the partial update that potentially was passed. --- lib/client-api/src/decorators.test.ts | 19 +++++++++++ lib/client-api/src/decorators.ts | 48 ++++++++++++++------------- 2 files changed, 44 insertions(+), 23 deletions(-) diff --git a/lib/client-api/src/decorators.test.ts b/lib/client-api/src/decorators.test.ts index b913c700b7b5..e4ebe6506010 100644 --- a/lib/client-api/src/decorators.test.ts +++ b/lib/client-api/src/decorators.test.ts @@ -42,6 +42,25 @@ describe('client-api.decorators', () => { expect(contexts.map((c) => c.k)).toEqual([0, 3, 2, 1]); }); + it('passes context through to sub decorators additively', () => { + const contexts = []; + const decorators = [ + (s, c) => contexts.push(c) && s({ b: 1 }), + (s, c) => contexts.push(c) && s({ c: 2 }), + (s, c) => contexts.push(c) && s({ d: 3 }), + ]; + const decorated = defaultDecorateStory((c) => contexts.push(c), decorators); + + expect(contexts).toEqual([]); + decorated(makeContext({ a: 0 })); + expect(contexts.map(({ a, b, c, d }) => ({ a, b, c, d }))).toEqual([ + { a: 0, b: undefined, c: undefined, d: undefined }, + { a: 0, b: undefined, c: undefined, d: 3 }, + { a: 0, b: undefined, c: 2, d: 3 }, + { a: 0, b: 1, c: 2, d: 3 }, + ]); + }); + it('does not recreate decorated story functions each time', () => { const decoratedStories = []; const decorators = [ diff --git a/lib/client-api/src/decorators.ts b/lib/client-api/src/decorators.ts index f0a36010aee5..d5fa38ff88fb 100644 --- a/lib/client-api/src/decorators.ts +++ b/lib/client-api/src/decorators.ts @@ -11,37 +11,21 @@ const defaultContext: StoryContext = { globals: {}, }; -/** - * When you call the story function inside a decorator, e.g.: - * - * ```jsx - *
{storyFn({ foo: 'bar' })}
- * ``` - * - * This will override the `foo` property on the `innerContext`, which gets - * merged in with the default context - */ -const bindWithContext = ( - storyFn: LegacyStoryFn, - getStoryContext: () => StoryContext -): PartialStoryFn => - // (NOTE: You cannot override the parameters key, it is fixed) - ({ id, name, kind, parameters, ...contextUpdate }: StoryContextUpdate = {}) => - storyFn({ ...getStoryContext(), ...contextUpdate }); - export const decorateStory = ( storyFn: LegacyStoryFn, decorator: DecoratorFunction, - getStoryContext: () => StoryContext + bindWithContext: (storyFn: LegacyStoryFn) => PartialStoryFn ): LegacyStoryFn => { // Bind the partially decorated storyFn so that when it is called it always knows about the story context, // no matter what it is passed directly. This is because we cannot guarantee a decorator will // pass the context down to the next decorated story in the chain. - const boundStoryFunction = bindWithContext(storyFn, getStoryContext); + const boundStoryFunction = bindWithContext(storyFn); return (context: StoryContext) => decorator(boundStoryFunction, context); }; +type ContextStore = { value: StoryContext }; + export const defaultDecorateStory = ( storyFn: LegacyStoryFn, decorators: DecoratorFunction[] @@ -52,13 +36,31 @@ export const defaultDecorateStory = ( // (ie to this story), so there is no possibility of overlap. // This will break if you call the same story twice interleaved // (React might do it if you rendered the same story twice in the one ReactDom.render call, for instance) - let contextStore: StoryContext; + const contextStore: ContextStore = { value: defaultContext }; + + /** + * When you call the story function inside a decorator, e.g.: + * + * ```jsx + *
{storyFn({ foo: 'bar' })}
+ * ``` + * + * This will override the `foo` property on the `innerContext`, which gets + * merged in with the default context + */ + const bindWithContext = (decoratedStoryFn: LegacyStoryFn): PartialStoryFn => + // (NOTE: You cannot override the parameters key, it is fixed) + ({ id, name, kind, parameters, ...contextUpdate }: StoryContextUpdate = {}) => { + contextStore.value = { ...contextStore.value, ...contextUpdate }; + return decoratedStoryFn(contextStore.value); + }; + const decoratedWithContextStore = decorators.reduce( - (story, decorator) => decorateStory(story, decorator, () => contextStore), + (story, decorator) => decorateStory(story, decorator, bindWithContext), storyFn ); return (context = defaultContext) => { - contextStore = context; + contextStore.value = context; return decoratedWithContextStore(context); // Pass the context directly into the first decorator }; };