Merge pull request #10801 from storybookjs/tech/cleanup-docs-code-fix-zoom

FIX zoom in docs
This commit is contained in:
Norbert de Langen 2020-05-19 09:40:32 +02:00 committed by GitHub
commit 0f4e98d009
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 179 additions and 116 deletions

View File

@ -1,7 +1,7 @@
import React, { FunctionComponent, ReactNode } from 'react';
import React, { FunctionComponent, ReactNode, ComponentProps } from 'react';
import { MDXProvider } from '@mdx-js/react';
import { resetComponents } from '@storybook/components/html';
import { Story as PureStory, StoryProps as PureStoryProps } from '@storybook/components';
import { Story as PureStory } from '@storybook/components';
import { toId, storyNameFromExport } from '@storybook/csf';
import { CURRENT_SELECTION } from './types';
@ -9,6 +9,8 @@ import { DocsContext, DocsContextProps } from './DocsContext';
export const storyBlockIdFromId = (storyId: string) => `story--${storyId}`;
type PureStoryProps = ComponentProps<typeof PureStory>;
interface CommonProps {
height?: string;
inline?: boolean;
@ -48,14 +50,13 @@ export const getStoryProps = (props: StoryProps, context: DocsContextProps): Pur
const { name } = props as StoryDefProps;
const inputId = id === CURRENT_SELECTION ? context.id : id;
const previewId = inputId || lookupStoryId(name, context);
const data = context.storyStore.fromId(previewId) || {};
const { height, inline } = props;
const data = context.storyStore.fromId(previewId);
const { framework = null } = (data && data.parameters) || {};
const { parameters = {}, docs = {} } = data;
const { framework = null } = parameters;
const docsParam = (data && data.parameters && data.parameters.docs) || {};
if (docsParam.disable) {
if (docs.disable) {
return null;
}
@ -64,8 +65,8 @@ export const getStoryProps = (props: StoryProps, context: DocsContextProps): Pur
inlineStories = inferInlineStories(framework),
iframeHeight = undefined,
prepareForInline = undefined,
} = docsParam;
const { storyFn = undefined, name: storyName = undefined } = data || {};
} = docs;
const { storyFn = undefined, name: storyName = undefined } = data;
const storyIsInline = typeof inline === 'boolean' ? inline : inlineStories;
if (storyIsInline && !prepareForInline && framework !== 'react') {
@ -75,6 +76,7 @@ export const getStoryProps = (props: StoryProps, context: DocsContextProps): Pur
}
return {
parameters,
inline: storyIsInline,
id: previewId,
storyFn: prepareForInline && storyFn ? () => prepareForInline(storyFn) : storyFn,

View File

@ -14,4 +14,4 @@ import { Meta, DocsContainer } from '@storybook/addon-docs/blocks';
}}
/>
<Story name='dummy'><div>some content</div></Story>
<Story name='dummy' parameters={{ layout: 'fullscreen' }}><div>some content</div></Story>

View File

@ -5,6 +5,9 @@ import markdown from './markdown.stories.mdx';
export default {
title: 'Addons/Docs/mdx-in-story',
decorators: [(storyFn) => <DocsContainer context={{}}>{storyFn()}</DocsContainer>],
parameters: {
layout: 'fullscreen',
},
};
// This renders the contents of the docs panel into story content

View File

@ -1,24 +1,44 @@
import { Meta, Preview, Story } from '@storybook/addon-docs/blocks';
import { Button } from '@storybook/react/demo';
<Meta
title="Core/Layout MDX"
component={Button}
id="core-layout-mdx"
decorators={[storyFn => <div style={{ backgroundColor: 'yellow' }}>{storyFn()}</div>]}
decorators={[(storyFn) => <div style={{ backgroundColor: 'yellow' }}>{storyFn()}</div>]}
/>
# Selected
# Layout parameter
<Preview>
This tests Storybook's built-in `layout` parameter, both as its applied in the canvas, and also how it's handled by the `Preview` block in `addon-docs`.
## Default
<Preview withToolbar>
<Story name="defaultValue">
<Button>Hello world</Button>
</Story>
</Preview>
## Padded
<Preview withToolbar>
<Story name="padded" parameters={{ layout: 'padded' }}>
<Button>Hello world</Button>
</Story>
</Preview>
## Fullscreen
<Preview withToolbar>
<Story name="fullscreen" parameters={{ layout: 'fullscreen' }}>
<Button>Hello world</Button>
</Story>
</Preview>
## Centered
<Preview withToolbar>
<Story name="centered" parameters={{ layout: 'centered' }}>
<Button>Hello world</Button>
</Story>

View File

@ -2,7 +2,7 @@ import React from 'react';
import { useEffect, useRef, useState } from '@storybook/client-api';
export default {
title: 'Hooks',
title: 'Core/Hooks',
};
export const Checkbox = () => {

View File

@ -9,6 +9,9 @@ import * as descriptionStories from './Description.stories';
export default {
title: 'Docs/DocsPage',
component: DocsWrapper,
parameters: {
layout: 'fullscreen',
},
decorators: [
(storyFn) => (
<DocsWrapper>

View File

@ -110,3 +110,29 @@ export const withToolbarMulti = () => (
<Story inline storyFn={buttonFn} title="story2" />
</Preview>
);
export const withFullscreenSingle = () => (
<Preview withToolbar>
<Story inline storyFn={buttonFn} title="story1" parameters={{ layout: 'fullscreen' }} />
</Preview>
);
export const withFullscreenMulti = () => (
<Preview withToolbar>
<Story inline storyFn={buttonFn} title="story1" parameters={{ layout: 'fullscreen' }} />
<Story inline storyFn={buttonFn} title="story2" parameters={{ layout: 'fullscreen' }} />
</Preview>
);
export const withCenteredSingle = () => (
<Preview withToolbar>
<Story inline storyFn={buttonFn} title="story1" parameters={{ layout: 'centered' }} />
</Preview>
);
export const withCenteredMulti = () => (
<Preview withToolbar>
<Story inline storyFn={buttonFn} title="story1" parameters={{ layout: 'centered' }} />
<Story inline storyFn={buttonFn} title="story2" parameters={{ layout: 'centered' }} />
</Preview>
);

View File

@ -1,12 +1,12 @@
import React, { Children, FunctionComponent, ReactElement, ReactNode, useState } from 'react';
import { styled } from '@storybook/theming';
import { darken } from 'polished';
import { logger } from '@storybook/client-logger';
import { styled } from '@storybook/theming';
import { getBlockBackgroundStyle } from './BlockBackgroundStyles';
import { Source, SourceProps } from './Source';
import { ActionBar, ActionItem } from '../ActionBar/ActionBar';
import { Toolbar } from './Toolbar';
import { ZoomContext } from './ZoomContext';
export interface PreviewProps {
isColumn?: boolean;
@ -17,20 +17,54 @@ export interface PreviewProps {
className?: string;
}
const ChildrenContainer = styled.div<PreviewProps>(({ isColumn, columns }) => ({
display: 'flex',
position: 'relative',
flexWrap: 'wrap',
padding: '10px 20px 30px 20px',
overflow: 'auto',
flexDirection: isColumn ? 'column' : 'row',
type layout = 'padded' | 'fullscreen' | 'centered';
'> *': {
flex: columns ? `1 1 calc(100%/${columns} - 20px)` : `1 1 0%`,
marginTop: 20,
maxWidth: '100%',
},
}));
const ChildrenContainer = styled.div<PreviewProps & { zoom: number; layout: layout }>(
({ isColumn, columns }) => ({
display: isColumn || !columns ? 'block' : 'flex',
position: 'relative',
flexWrap: 'wrap',
overflow: 'auto',
flexDirection: isColumn ? 'column' : 'row',
'& > *': isColumn
? {
width: '100%',
display: 'block',
}
: {
maxWidth: '100%',
display: 'inline-block',
},
}),
({ layout = 'padded' }) =>
layout === 'centered' || layout === 'padded'
? {
padding: '30px 20px',
margin: -10,
'& > *': {
border: '10px solid transparent!important',
},
}
: {},
({ layout = 'padded' }) =>
layout === 'centered'
? {
display: 'flex',
justifyContent: 'center',
justifyItems: 'center',
alignContent: 'center',
alignItems: 'center',
}
: {},
({ zoom = 1 }) => ({
'> *': {
zoom: 1 / zoom,
},
}),
({ columns }) =>
columns && columns > 1 ? { '> *': { minWidth: `calc(100% / ${columns} - 20px)` } } : {}
);
const StyledSource = styled(Source)<{}>(({ theme }) => ({
margin: 0,
@ -107,23 +141,6 @@ function getStoryId(children: ReactNode) {
return null;
}
const Relative = styled.div({
position: 'relative',
});
const Scale = styled.div<{ scale: number }>(
{
position: 'relative',
},
({ scale }) =>
scale
? {
transform: `scale(${1 / scale})`,
transformOrigin: 'top left',
}
: {}
);
const PositionedToolbar = styled(Toolbar)({
position: 'absolute',
top: 0,
@ -132,6 +149,23 @@ const PositionedToolbar = styled(Toolbar)({
height: 40,
});
const Relative = styled.div({
overflow: 'hidden',
position: 'relative',
});
const getLayout = (children: ReactElement[]) => {
return children.reduce((result, c) => {
if (result) {
return result;
}
if (typeof c === 'string' || typeof c === 'number') {
return 'padded';
}
return (c.props && c.props.parameters && c.props.parameters.layout) || 'padded';
}, undefined);
};
/**
* A preview component for showing one or more component `Story`
* items. The preview also shows the source for the component
@ -150,19 +184,18 @@ const Preview: FunctionComponent<PreviewProps> = ({
const [expanded, setExpanded] = useState(isExpanded);
const { source, actionItem } = getSource(withSource, expanded, setExpanded);
const [scale, setScale] = useState(1);
const previewClasses = className ? `${className} sbdocs sbdocs-preview` : 'sbdocs sbdocs-preview';
const previewClasses = [className].concat(['sbdocs', 'sbdocs-preview']);
// @ts-ignore
const layout = getLayout(Children.count(children) === 1 ? [children] : children);
if (withToolbar && Array.isArray(children)) {
logger.warn('Cannot use toolbar with multiple preview children, disabling');
}
const showToolbar = withToolbar && !Array.isArray(children);
return (
<PreviewContainer
{...{ withSource, withToolbar: showToolbar }}
{...{ withSource, withToolbar }}
{...props}
className={previewClasses}
className={previewClasses.join(' ')}
>
{showToolbar && (
{withToolbar && (
<PositionedToolbar
border
zoom={(z) => setScale(scale * z)}
@ -171,16 +204,19 @@ const Preview: FunctionComponent<PreviewProps> = ({
baseUrl="./iframe.html"
/>
)}
<Relative>
<ChildrenContainer isColumn={isColumn} columns={columns}>
{Array.isArray(children) ? (
children.map((child, i) => <div key={i.toString()}>{child}</div>)
) : (
<Scale scale={scale}>{children}</Scale>
)}
</ChildrenContainer>
{withSource && <ActionBar actionItems={[actionItem]} />}
</Relative>
<ZoomContext.Provider value={{ scale }}>
<Relative>
<ChildrenContainer isColumn={isColumn} columns={columns} zoom={scale} layout={layout}>
{Array.isArray(children) ? (
// eslint-disable-next-line react/no-array-index-key
children.map((child, i) => <div key={i}>{child}</div>)
) : (
<div>{children}</div>
)}
</ChildrenContainer>
{withSource && <ActionBar actionItems={[actionItem]} />}
</Relative>
</ZoomContext.Provider>
{withSource && source}
</PreviewContainer>
);

View File

@ -1,4 +1,8 @@
import React, { createElement, ElementType, FunctionComponent } from 'react';
import React, { createElement, ElementType, FunctionComponent, Fragment } from 'react';
// eslint-disable-next-line import/no-extraneous-dependencies
import { Parameters } from '@storybook/api';
import { IFrame } from './IFrame';
import { EmptyBlock } from './EmptyBlock';
import { ZoomContext } from './ZoomContext';
@ -21,48 +25,22 @@ interface CommonProps {
id: string;
}
type InlineStoryProps = {
interface InlineStoryProps extends CommonProps {
parameters: Parameters;
storyFn: ElementType;
} & CommonProps;
}
type IFrameStoryProps = CommonProps;
type ErrorProps = {
error?: StoryError;
} & CommonProps;
// How do you XOR properties in typescript?
export type StoryProps = (InlineStoryProps | IFrameStoryProps | ErrorProps) & {
inline: boolean;
};
const InlineZoomWrapper: FunctionComponent<{ scale: number }> = ({ scale, children }) => {
return scale === 1 ? (
<>{children}</>
) : (
<div style={{ overflow: 'hidden' }}>
<div
style={{
transform: `scale(${1 / scale})`,
transformOrigin: 'top left',
}}
>
{children}
</div>
</div>
);
};
type StoryProps = InlineStoryProps | IFrameStoryProps;
const InlineStory: FunctionComponent<InlineStoryProps> = ({ storyFn, height, id }) => (
<div style={{ height }}>
<ZoomContext.Consumer>
{({ scale }) => (
<InlineZoomWrapper scale={scale}>
{storyFn ? createElement(storyFn) : <EmptyBlock>{MISSING_STORY(id)}</EmptyBlock>}
</InlineZoomWrapper>
)}
</ZoomContext.Consumer>
</div>
<Fragment>
{height ? <style>{`#story--${id} { min-height: ${height} }`}</style> : null}
<Fragment>
{storyFn ? createElement(storyFn) : <EmptyBlock>{MISSING_STORY(id)}</EmptyBlock>}
</Fragment>
</Fragment>
);
const IFrameStory: FunctionComponent<IFrameStoryProps> = ({ id, title, height = '500px' }) => (
@ -90,19 +68,22 @@ const IFrameStory: FunctionComponent<IFrameStoryProps> = ({ id, title, height =
);
/**
* A story element, either renderend inline or in an iframe,
* A story element, either rendered inline or in an iframe,
* with configurable height.
*/
const Story: FunctionComponent<StoryProps> = (props) => {
const { error } = props as ErrorProps;
const { storyFn } = props as InlineStoryProps;
const { id, inline, title, height } = props;
const Story: FunctionComponent<StoryProps & { inline: boolean; error?: StoryError }> = ({
children,
error,
inline,
...props
}) => {
const { id, title, height } = props;
if (error) {
return <EmptyBlock>{error}</EmptyBlock>;
}
return inline ? (
<InlineStory id={id} storyFn={storyFn} title={title} height={height} />
<InlineStory {...(props as InlineStoryProps)} />
) : (
<IFrameStory id={id} title={title} height={height} />
);

View File

@ -7,15 +7,7 @@ import markdownSample from './DocumentFormattingSample.md';
export default {
component: DocumentWrapper,
title: 'Basics/DocumentFormatting',
decorators: [
(storyFn: any) => (
<div
style={{ width: '600px', margin: '3rem auto', padding: '40px 20px', background: 'white' }}
>
{storyFn()}
</div>
),
],
decorators: [(storyFn: any) => <div style={{ width: '600px' }}>{storyFn()}</div>],
};
export const withMarkdown = () => (