storyBook.tsx 2.2 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788
  1. import {Fragment, ReactNode, useCallback, useEffect, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import Placeholder from 'sentry/components/placeholder';
  4. import SideBySide from 'sentry/components/stories/sideBySide';
  5. import {space} from 'sentry/styles/space';
  6. type RenderFn = () => ReactNode | ReactNode[] | Promise<ReactNode> | Promise<ReactNode[]>;
  7. type StoryFn = (storyName: string, storyRender: RenderFn) => Promise<void>;
  8. type SetupFn = (story: StoryFn) => void | Promise<void>;
  9. type Context = {
  10. children: ReactNode[];
  11. isResolved: boolean;
  12. rootName: string;
  13. storyName: string;
  14. };
  15. type ContextMap = Record<string, Context>;
  16. export default function storyBook(rootName: string, setup: SetupFn) {
  17. return function RenderStory() {
  18. const [contexts, setContexts] = useState<ContextMap>({});
  19. const storyFn = useCallback(async (storyName: string, storyRender: RenderFn) => {
  20. const context: Context = {
  21. rootName,
  22. storyName,
  23. children: [<Placeholder key="placeholder" />],
  24. isResolved: false,
  25. };
  26. setContexts(prev => ({
  27. ...prev,
  28. [storyName]: context,
  29. }));
  30. const finished = await storyRender();
  31. context.children = [finished];
  32. setContexts(prev => ({...prev}));
  33. }, []);
  34. useEffect(() => {
  35. setup(storyFn);
  36. }, [storyFn]);
  37. const items = Object.values(contexts).map(context => (
  38. <Section
  39. key={context.rootName + context.storyName}
  40. rootName={context.rootName}
  41. name={context.storyName}
  42. >
  43. {doRender(context.children)}
  44. </Section>
  45. ));
  46. return <Fragment>{items}</Fragment>;
  47. };
  48. }
  49. function doRender(children: ReactNode | ReactNode[]) {
  50. if (Array.isArray(children)) {
  51. return <SideBySide>{children}</SideBySide>;
  52. }
  53. return function () {
  54. return children;
  55. };
  56. }
  57. interface SectionProps {
  58. children: ReactNode;
  59. name: string;
  60. rootName: string;
  61. }
  62. function Section({rootName, name, children}: SectionProps) {
  63. return (
  64. <Story>
  65. <StoryTitle id={`${rootName}_${name}`}>{name}</StoryTitle>
  66. {children}
  67. </Story>
  68. );
  69. }
  70. const Story = styled('section')`
  71. margin-bottom: ${space(4)};
  72. `;
  73. const StoryTitle = styled('h4')`
  74. border-bottom: 1px solid ${p => p.theme.border};
  75. `;