UNPKG

10.4 kBJavaScriptView Raw
1import "core-js/modules/es.array.reduce.js";
2import React, { Component, Fragment, useCallback, useContext, useEffect, useMemo, useRef } from 'react';
3import mergeWith from 'lodash/mergeWith';
4import { STORY_CHANGED, SHARED_STATE_CHANGED, SHARED_STATE_SET, SET_STORIES } from '@storybook/core-events';
5import { createContext } from './context';
6import Store from './store';
7import getInitialState from './initial-state';
8import { isGroup, isRoot, isStory } from './lib/stories';
9import * as provider from './modules/provider';
10import * as addons from './modules/addons';
11import * as channel from './modules/channel';
12import * as notifications from './modules/notifications';
13import * as settings from './modules/settings';
14import * as releaseNotes from './modules/release-notes';
15import * as stories from './modules/stories';
16import * as refs from './modules/refs';
17import * as layout from './modules/layout';
18import * as shortcuts from './modules/shortcuts';
19import * as url from './modules/url';
20import * as version from './modules/versions';
21import * as globals from './modules/globals';
22const {
23 ActiveTabs
24} = layout;
25export { default as merge } from './lib/merge';
26export { ActiveTabs };
27const ManagerContext = createContext({
28 api: undefined,
29 state: getInitialState({})
30});
31// This is duplicated from @storybook/client-api for the reasons mentioned in lib-addons/types.js
32export const combineParameters = (...parameterSets) => mergeWith({}, ...parameterSets, (objValue, srcValue) => {
33 // Treat arrays as scalars:
34 if (Array.isArray(srcValue)) return srcValue;
35 return undefined;
36});
37
38class ManagerProvider extends Component {
39 constructor(props) {
40 super(props);
41 this.api = {};
42 this.modules = void 0;
43
44 this.initModules = () => {
45 // Now every module has had a chance to set its API, call init on each module which gives it
46 // a chance to do things that call other modules' APIs.
47 this.modules.forEach(({
48 init
49 }) => {
50 if (init) {
51 init();
52 }
53 });
54 };
55
56 const {
57 location,
58 path,
59 refId,
60 viewMode = props.docsMode ? 'docs' : 'story',
61 singleStory,
62 storyId,
63 docsMode,
64 navigate
65 } = props;
66 const store = new Store({
67 getState: () => this.state,
68 setState: (stateChange, callback) => this.setState(stateChange, callback)
69 });
70 const routeData = {
71 location,
72 path,
73 viewMode,
74 singleStory,
75 storyId,
76 refId
77 }; // Initialize the state to be the initial (persisted) state of the store.
78 // This gives the modules the chance to read the persisted state, apply their defaults
79 // and override if necessary
80
81 const docsModeState = {
82 layout: {
83 showToolbar: false,
84 showPanel: false
85 },
86 ui: {
87 docsMode: true
88 }
89 };
90 this.state = store.getInitialState(getInitialState(Object.assign({}, routeData, docsMode ? docsModeState : null)));
91 const apiData = {
92 navigate,
93 store,
94 provider: props.provider
95 };
96 this.modules = [provider, channel, addons, layout, notifications, settings, releaseNotes, shortcuts, stories, refs, globals, url, version].map(m => m.init(Object.assign({}, routeData, apiData, {
97 state: this.state,
98 fullAPI: this.api
99 }))); // Create our initial state by combining the initial state of all modules, then overlaying any saved state
100
101 const state = getInitialState(this.state, ...this.modules.map(m => m.state)); // Get our API by combining the APIs exported by each module
102
103 const api = Object.assign(this.api, {
104 navigate
105 }, ...this.modules.map(m => m.api));
106 this.state = state;
107 this.api = api;
108 }
109
110 static getDerivedStateFromProps(props, state) {
111 if (state.path !== props.path) {
112 return Object.assign({}, state, {
113 location: props.location,
114 path: props.path,
115 refId: props.refId,
116 // if its a docsOnly page, even the 'story' view mode is considered 'docs'
117 viewMode: (props.docsMode && props.viewMode) === 'story' ? 'docs' : props.viewMode,
118 storyId: props.storyId
119 });
120 }
121
122 return null;
123 }
124
125 shouldComponentUpdate(nextProps, nextState) {
126 const prevState = this.state;
127 const prevProps = this.props;
128
129 if (prevState !== nextState) {
130 return true;
131 }
132
133 if (prevProps.path !== nextProps.path) {
134 return true;
135 }
136
137 return false;
138 }
139
140 render() {
141 const {
142 children
143 } = this.props;
144 const value = {
145 state: this.state,
146 api: this.api
147 };
148 return /*#__PURE__*/React.createElement(EffectOnMount, {
149 effect: this.initModules
150 }, /*#__PURE__*/React.createElement(ManagerContext.Provider, {
151 value: value
152 }, /*#__PURE__*/React.createElement(ManagerConsumer, null, children)));
153 }
154
155}
156
157ManagerProvider.displayName = "ManagerProvider";
158ManagerProvider.displayName = 'Manager';
159
160// EffectOnMount exists to work around a bug in Reach Router where calling
161// navigate inside of componentDidMount (as could happen when we call init on any
162// of our modules) does not cause Reach Router's LocationProvider to update with
163// the correct path. Calling navigate inside on an effect does not have the
164// same problem. See https://github.com/reach/router/issues/404
165const EffectOnMount = ({
166 children,
167 effect
168}) => {
169 React.useEffect(effect, []);
170 return children;
171};
172
173const defaultFilter = c => c;
174
175function ManagerConsumer({
176 // @ts-ignore
177 filter = defaultFilter,
178 children
179}) {
180 const c = useContext(ManagerContext);
181 const renderer = useRef(children);
182 const filterer = useRef(filter);
183
184 if (typeof renderer.current !== 'function') {
185 return /*#__PURE__*/React.createElement(Fragment, null, renderer.current);
186 }
187
188 const data = filterer.current(c);
189 const l = useMemo(() => {
190 return [...Object.entries(data).reduce((acc, keyval) => acc.concat(keyval), [])];
191 }, [c.state]);
192 return useMemo(() => {
193 const Child = renderer.current;
194 return /*#__PURE__*/React.createElement(Child, data);
195 }, l);
196}
197
198export function useStorybookState() {
199 const {
200 state
201 } = useContext(ManagerContext);
202 return state;
203}
204export function useStorybookApi() {
205 const {
206 api
207 } = useContext(ManagerContext);
208 return api;
209}
210export { ManagerConsumer as Consumer, ManagerProvider as Provider, isGroup, isRoot, isStory };
211
212function orDefault(fromStore, defaultState) {
213 if (typeof fromStore === 'undefined') {
214 return defaultState;
215 }
216
217 return fromStore;
218}
219
220export const useChannel = (eventMap, deps = []) => {
221 const api = useStorybookApi();
222 useEffect(() => {
223 Object.entries(eventMap).forEach(([type, listener]) => api.on(type, listener));
224 return () => {
225 Object.entries(eventMap).forEach(([type, listener]) => api.off(type, listener));
226 };
227 }, deps);
228 return api.emit;
229};
230export function useStoryPrepared(storyId) {
231 const api = useStorybookApi();
232 return api.isPrepared(storyId);
233}
234export function useParameter(parameterKey, defaultValue) {
235 const api = useStorybookApi();
236 const result = api.getCurrentParameter(parameterKey);
237 return orDefault(result, defaultValue);
238}
239// cache for taking care of HMR
240const addonStateCache = {}; // shared state
241
242export function useSharedState(stateId, defaultState) {
243 const api = useStorybookApi();
244 const existingState = api.getAddonState(stateId);
245 const state = orDefault(existingState, addonStateCache[stateId] ? addonStateCache[stateId] : defaultState);
246
247 const setState = (s, options) => {
248 // set only after the stories are loaded
249 if (addonStateCache[stateId]) {
250 addonStateCache[stateId] = s;
251 }
252
253 api.setAddonState(stateId, s, options);
254 };
255
256 const allListeners = useMemo(() => {
257 const stateChangeHandlers = {
258 [`${SHARED_STATE_CHANGED}-client-${stateId}`]: s => setState(s),
259 [`${SHARED_STATE_SET}-client-${stateId}`]: s => setState(s)
260 };
261 const stateInitializationHandlers = {
262 [SET_STORIES]: () => {
263 const currentState = api.getAddonState(stateId);
264
265 if (currentState) {
266 addonStateCache[stateId] = currentState;
267 api.emit(`${SHARED_STATE_SET}-manager-${stateId}`, currentState);
268 } else if (addonStateCache[stateId]) {
269 // this happens when HMR
270 setState(addonStateCache[stateId]);
271 api.emit(`${SHARED_STATE_SET}-manager-${stateId}`, addonStateCache[stateId]);
272 } else if (defaultState !== undefined) {
273 // if not HMR, yet the defaults are from the manager
274 setState(defaultState); // initialize addonStateCache after first load, so its available for subsequent HMR
275
276 addonStateCache[stateId] = defaultState;
277 api.emit(`${SHARED_STATE_SET}-manager-${stateId}`, defaultState);
278 }
279 },
280 [STORY_CHANGED]: () => {
281 const currentState = api.getAddonState(stateId);
282
283 if (currentState !== undefined) {
284 api.emit(`${SHARED_STATE_SET}-manager-${stateId}`, currentState);
285 }
286 }
287 };
288 return Object.assign({}, stateChangeHandlers, stateInitializationHandlers);
289 }, [stateId]);
290 const emit = useChannel(allListeners);
291 return [state, (newStateOrMerger, options) => {
292 setState(newStateOrMerger, options);
293 emit(`${SHARED_STATE_CHANGED}-manager-${stateId}`, newStateOrMerger);
294 }];
295}
296export function useAddonState(addonId, defaultState) {
297 return useSharedState(addonId, defaultState);
298}
299export function useArgs() {
300 const {
301 getCurrentStoryData,
302 updateStoryArgs,
303 resetStoryArgs
304 } = useStorybookApi();
305 const data = getCurrentStoryData();
306 const args = isStory(data) ? data.args : {};
307 const updateArgs = useCallback(newArgs => updateStoryArgs(data, newArgs), [data, updateStoryArgs]);
308 const resetArgs = useCallback(argNames => resetStoryArgs(data, argNames), [data, resetStoryArgs]);
309 return [args, updateArgs, resetArgs];
310}
311export function useGlobals() {
312 const api = useStorybookApi();
313 return [api.getGlobals(), api.updateGlobals];
314}
315export function useGlobalTypes() {
316 return useStorybookApi().getGlobalTypes();
317}
318
319function useCurrentStory() {
320 const {
321 getCurrentStoryData
322 } = useStorybookApi();
323 return getCurrentStoryData();
324}
325
326export function useArgTypes() {
327 var _useCurrentStory;
328
329 return ((_useCurrentStory = useCurrentStory()) === null || _useCurrentStory === void 0 ? void 0 : _useCurrentStory.argTypes) || {};
330}
\No newline at end of file