MIGRATE more of lib/ui

This commit is contained in:
Norbert de Langen 2020-02-07 23:13:12 +01:00
parent 4e61e0ef5f
commit 1f940ff62c
No known key found for this signature in database
GPG Key ID: 976651DA156C2825
13 changed files with 229 additions and 236 deletions

View File

@ -2,6 +2,8 @@ import { Module } from '../index';
export interface Notification {
id: string;
link: string;
content: string;
onClear?: () => void;
}

View File

@ -139,7 +139,7 @@ export interface TabsProps {
selected?: string;
actions?: {
onSelect: (id: string) => void;
};
} & Record<string, any>;
backgroundColor?: string;
absolute?: boolean;
bordered?: boolean;

View File

@ -1,9 +1,9 @@
import React from 'react';
import PropTypes from 'prop-types';
import React, { FunctionComponent } from 'react';
import { State } from '@storybook/api';
import { styled, lighten, darken } from '@storybook/theming';
import { Link } from '@storybook/router';
const baseStyle = ({ theme }) => ({
const Notification = styled.div(({ theme }) => ({
display: 'block',
padding: '16px 20px',
borderRadius: 10,
@ -15,26 +15,21 @@ const baseStyle = ({ theme }) => ({
backgroundColor:
theme.base === 'light' ? darken(theme.background.app) : lighten(theme.background.app),
textDecoration: 'none',
});
const NotificationLink = styled(Link)(baseStyle);
const Notification = styled.div(baseStyle);
}));
const NotificationLink = Notification.withComponent(Link);
export const NotificationItemSpacer = styled.div({
height: 48,
});
export default function NotificationItem({ notification: { content, link } }) {
const NotificationItem: FunctionComponent<{
notification: State['notifications'][0];
}> = ({ notification: { content, link } }) => {
return link ? (
<NotificationLink to={link}>{content}</NotificationLink>
) : (
<Notification>{content}</Notification>
);
}
NotificationItem.propTypes = {
notification: PropTypes.shape({
content: PropTypes.string.isRequired,
link: PropTypes.string,
}).isRequired,
};
export default NotificationItem;

View File

@ -1,55 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { styled } from '@storybook/theming';
import NotificationItem from './item';
const List = styled.div(
{
zIndex: 10,
'> * + *': {
marginTop: 10,
},
'&:empty': {
display: 'none',
},
},
({ placement }) =>
placement || {
bottom: 0,
left: 0,
right: 0,
position: 'fixed',
}
);
export default function NotificationList({ notifications, placement }) {
return (
<List placement={placement}>
{notifications.map(notification => (
<NotificationItem key={notification.id} notification={notification} />
))}
</List>
);
}
NotificationList.propTypes = {
placement: PropTypes.shape({
position: PropTypes.string,
left: PropTypes.number,
right: PropTypes.number,
top: PropTypes.number,
bottom: PropTypes.number,
}),
notifications: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.string,
content: PropTypes.string.isRequired,
link: PropTypes.string,
}).isRequired
).isRequired,
};
NotificationList.defaultProps = {
placement: undefined,
};

View File

@ -0,0 +1,40 @@
import React, { FunctionComponent } from 'react';
import { State } from '@storybook/api';
import { styled, CSSObject } from '@storybook/theming';
import NotificationItem from './item';
const List = styled.div<{ placement?: CSSObject }>(
{
zIndex: 10,
'> * + *': {
marginTop: 10,
},
'&:empty': {
display: 'none',
},
},
({ placement }) =>
placement || {
bottom: 0,
left: 0,
right: 0,
position: 'fixed',
}
);
const NotificationList: FunctionComponent<{
placement: CSSObject;
notifications: State['notifications'];
}> = ({ notifications, placement = undefined }) => {
return (
<List placement={placement}>
{notifications.map(notification => (
<NotificationItem key={notification.id} notification={notification} />
))}
</List>
);
};
export default NotificationList;

View File

@ -1,99 +0,0 @@
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import { styled } from '@storybook/theming';
import { Tabs, Icons, IconButton } from '@storybook/components';
const DesktopOnlyIconButton = styled(IconButton)({
// Hides full screen icon at mobile breakpoint defined in app.js
'@media (max-width: 599px)': {
display: 'none',
},
});
class SafeTab extends Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
componentDidCatch(error, info) {
this.setState({ hasError: true });
// eslint-disable-next-line no-console
console.error(error, info);
}
render() {
const { hasError } = this.state;
const { children, title, id } = this.props;
if (hasError) {
return <h1>Something went wrong.</h1>;
}
return (
<div id={id} title={title}>
{children}
</div>
);
}
}
SafeTab.propTypes = {
children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
title: PropTypes.string.isRequired,
id: PropTypes.string.isRequired,
};
SafeTab.defaultProps = {
children: null,
};
const AddonPanel = React.memo(
({ panels, actions, selectedPanel, panelPosition, absolute = true }) => (
<Tabs
absolute={absolute}
selected={selectedPanel}
actions={actions}
flex
tools={
<Fragment>
<DesktopOnlyIconButton
key="position"
onClick={actions.togglePosition}
title="Change orientation"
>
<Icons icon={panelPosition === 'bottom' ? 'bottombar' : 'sidebaralt'} />
</DesktopOnlyIconButton>
<DesktopOnlyIconButton
key="visibility"
onClick={actions.toggleVisibility}
title="Hide addons"
>
<Icons icon="close" />
</DesktopOnlyIconButton>
</Fragment>
}
id="storybook-panel-root"
>
{Object.entries(panels).map(([k, v]) => (
<SafeTab key={k} id={k} title={v.title}>
{v.render}
</SafeTab>
))}
</Tabs>
)
);
AddonPanel.displayName = 'AddonPanel';
AddonPanel.propTypes = {
selectedPanel: PropTypes.string,
actions: PropTypes.shape({
togglePosition: PropTypes.func,
toggleVisibility: PropTypes.func,
}).isRequired,
panels: PropTypes.shape({}).isRequired,
panelPosition: PropTypes.oneOf(['bottom', 'right']),
absolute: PropTypes.bool,
};
AddonPanel.defaultProps = {
selectedPanel: null,
panelPosition: 'right',
absolute: true,
};
export default AddonPanel;

View File

@ -0,0 +1,83 @@
import React, { Component, Fragment } from 'react';
import { styled } from '@storybook/theming';
import { Tabs, Icons, IconButton } from '@storybook/components';
const DesktopOnlyIconButton = styled(IconButton)({
// Hides full screen icon at mobile breakpoint defined in app.js
'@media (max-width: 599px)': {
display: 'none',
},
});
interface SafeTabProps {
title: string;
id: string;
}
class SafeTab extends Component<SafeTabProps, { hasError: boolean }> {
constructor(props: SafeTabProps) {
super(props);
this.state = { hasError: false };
}
componentDidCatch(error: Error, info: any) {
this.setState({ hasError: true });
// eslint-disable-next-line no-console
console.error(error, info);
}
render() {
const { hasError } = this.state;
const { children, title, id } = this.props;
if (hasError) {
return <h1>Something went wrong.</h1>;
}
return (
<div id={id} title={title}>
{children}
</div>
);
}
}
const AddonPanel = React.memo<{
selectedPanel: string;
actions: { onSelect: (id: string) => void } & Record<string, any>;
panels: Record<string, any>;
panelPosition: 'bottom' | 'right';
absolute: boolean;
}>(({ panels, actions, selectedPanel = null, panelPosition = 'right', absolute = true }) => (
<Tabs
absolute={absolute}
selected={selectedPanel}
actions={actions}
tools={
<Fragment>
<DesktopOnlyIconButton
key="position"
onClick={actions.togglePosition}
title="Change orientation"
>
<Icons icon={panelPosition === 'bottom' ? 'bottombar' : 'sidebaralt'} />
</DesktopOnlyIconButton>
<DesktopOnlyIconButton
key="visibility"
onClick={actions.toggleVisibility}
title="Hide addons"
>
<Icons icon="close" />
</DesktopOnlyIconButton>
</Fragment>
}
id="storybook-panel-root"
>
{Object.entries(panels).map(([k, v]) => (
<SafeTab key={k} id={k} title={v.title}>
{v.render}
</SafeTab>
))}
</Tabs>
));
AddonPanel.displayName = 'AddonPanel';
export default AddonPanel;

View File

@ -1,10 +1,11 @@
import { DOCS_MODE } from 'global';
import React from 'react';
import React, { FunctionComponent } from 'react';
import memoize from 'memoizerific';
import { Badge } from '@storybook/components';
import { Consumer } from '@storybook/api';
import { Consumer, Combo } from '@storybook/api';
import { StoriesHash } from '@storybook/api/dist/modules/stories';
import { shortcutToHumanString } from '../libs/shortcut';
import ListItemIcon from '../components/sidebar/ListItemIcon';
@ -16,7 +17,7 @@ const focusableUIElements = {
storyPanelRoot: 'storybook-panel-root',
};
const shortcutToHumanStringIfEnabled = (shortcuts, enableShortcuts) =>
const shortcutToHumanStringIfEnabled = (shortcuts: string[], enableShortcuts: boolean) =>
enableShortcuts ? shortcutToHumanString(shortcuts) : null;
const createMenu = memoize(1)(
@ -108,9 +109,9 @@ const createMenu = memoize(1)(
]
);
export const collapseAllStories = stories => {
export const collapseAllStories = (stories: StoriesHash) => {
// keep track of component IDs that have been rewritten to the ID of their first leaf child
const componentIdToLeafId = {};
const componentIdToLeafId: Record<string, string> = {};
// 1) remove all leaves
const leavesRemoved = Object.values(stories).filter(
@ -126,8 +127,8 @@ export const collapseAllStories = stories => {
return item;
}
const nonLeafChildren = [];
const leafChildren = [];
const nonLeafChildren: string[] = [];
const leafChildren: string[] = [];
children.forEach(child => (stories[child].isLeaf ? leafChildren : nonLeafChildren).push(child));
if (leafChildren.length === 0) {
@ -135,7 +136,13 @@ export const collapseAllStories = stories => {
}
const leafId = leafChildren[0];
const component = { ...rest, id: leafId, isLeaf: true, isComponent: true };
const component = {
...rest,
id: leafId,
isLeaf: true,
isComponent: true,
children: [] as string[],
};
componentIdToLeafId[id] = leafId;
// this is a component, so it should not have any non-leaf children
@ -160,16 +167,16 @@ export const collapseAllStories = stories => {
return { children: rewritten, ...rest };
});
const result = {};
const result = {} as StoriesHash;
childrenRewritten.forEach(item => {
result[item.id] = item;
});
return result;
};
export const collapseDocsOnlyStories = storiesHash => {
export const collapseDocsOnlyStories = (storiesHash: StoriesHash) => {
// keep track of component IDs that have been rewritten to the ID of their first leaf child
const componentIdToLeafId = {};
const componentIdToLeafId: Record<string, string> = {};
const docsOnlyStoriesRemoved = Object.values(storiesHash).filter(item => {
if (item.isLeaf && item.parameters && item.parameters.docsOnly) {
componentIdToLeafId[item.parent] = item.id;
@ -187,7 +194,7 @@ export const collapseDocsOnlyStories = storiesHash => {
...item,
id: leafId,
isLeaf: true,
children: undefined,
children: [] as string[],
};
return collapsed;
}
@ -203,14 +210,14 @@ export const collapseDocsOnlyStories = storiesHash => {
return item;
});
const result = {};
const result = {} as StoriesHash;
docsOnlyComponentsCollapsed.forEach(item => {
result[item.id] = item;
});
return result;
};
export const mapper = ({ state, api }) => {
export const mapper = ({ state, api }: Combo) => {
const {
ui: { name, url, enableShortcuts },
viewMode,
@ -236,6 +243,10 @@ export const mapper = ({ state, api }) => {
};
};
export default props => (
<Consumer filter={mapper}>{fromState => <Sidebar {...props} {...fromState} />}</Consumer>
const Nav: FunctionComponent<any> = props => (
<Consumer filter={mapper}>
{(fromState: ReturnType<typeof mapper>) => <Sidebar {...props} {...fromState} />}
</Consumer>
);
export default Nav;

View File

@ -1,19 +0,0 @@
import React from 'react';
import { Consumer } from '@storybook/api';
import Notifications from '../components/notifications/notifications';
export const mapper = ({ state }) => {
const { notifications } = state;
return {
notifications,
};
};
const NotificationConnect = props => (
<Consumer filter={mapper}>{fromState => <Notifications {...props} {...fromState} />}</Consumer>
);
export default NotificationConnect;

View File

@ -0,0 +1,21 @@
import React, { FunctionComponent } from 'react';
import { Consumer, Combo } from '@storybook/api';
import Notifications from '../components/notifications/notifications';
export const mapper = ({ state }: Combo) => {
const { notifications } = state;
return {
notifications,
};
};
const NotificationConnect: FunctionComponent<any> = props => (
<Consumer filter={mapper}>
{(fromState: ReturnType<typeof mapper>) => <Notifications {...props} {...fromState} />}
</Consumer>
);
export default NotificationConnect;

View File

@ -1,22 +0,0 @@
import React from 'react';
import memoize from 'memoizerific';
import { Consumer } from '@storybook/api';
import AddonPanel from '../components/panel/panel';
const createPanelActions = memoize(1)(api => ({
onSelect: panel => api.setSelectedPanel(panel),
toggleVisibility: () => api.togglePanel(),
togglePosition: () => api.togglePanelPosition(),
}));
const mapper = ({ state, api }) => ({
panels: api.getStoryPanels(),
selectedPanel: api.getSelectedPanel(),
panelPosition: state.layout.panelPosition,
actions: createPanelActions(api),
});
export default props => (
<Consumer filter={mapper}>{customProps => <AddonPanel {...props} {...customProps} />}</Consumer>
);

View File

@ -0,0 +1,26 @@
import React, { FunctionComponent } from 'react';
import memoize from 'memoizerific';
import { Consumer, Combo } from '@storybook/api';
import AddonPanel from '../components/panel/panel';
const createPanelActions = memoize(1)(api => ({
onSelect: (panel: string) => api.setSelectedPanel(panel),
toggleVisibility: () => api.togglePanel(),
togglePosition: () => api.togglePanelPosition(),
}));
const mapper = ({ state, api }: Combo) => ({
panels: api.getStoryPanels(),
selectedPanel: api.getSelectedPanel(),
panelPosition: state.layout.panelPosition,
actions: createPanelActions(api),
});
const Panel: FunctionComponent<any> = props => (
<Consumer filter={mapper}>
{(customProps: ReturnType<typeof mapper>) => <AddonPanel {...props} {...customProps} />}
</Consumer>
);
export default Panel;

View File

@ -1,21 +1,31 @@
import { PREVIEW_URL } from 'global';
import React from 'react';
import { Consumer } from '@storybook/api';
import { Consumer, Combo } from '@storybook/api';
import { StoriesHash } from '@storybook/api/dist/modules/stories';
import { Preview } from '../components/preview/preview';
const nonAlphanumSpace = /[^a-z0-9 ]/gi;
const doubleSpace = /\s\s/gi;
const replacer = match => ` ${match} `;
const addExtraWhiteSpace = input =>
const replacer = (match: string) => ` ${match} `;
const addExtraWhiteSpace = (input: string) =>
input.replace(nonAlphanumSpace, replacer).replace(doubleSpace, ' ');
const getDescription = (storiesHash, storyId) => {
const getDescription = (storiesHash: StoriesHash, storyId: string) => {
const storyInfo = storiesHash[storyId];
return storyInfo ? addExtraWhiteSpace(`${storyInfo.kind} - ${storyInfo.name}`) : '';
if (storyInfo) {
// @ts-ignore
const { kind, name } = storyInfo;
return kind && name ? addExtraWhiteSpace(`${kind} - ${name}`) : '';
}
return '';
};
const mapper = ({ api, state }) => {
const mapper = ({ api, state }: Combo) => {
const { layout, location, customQueryParams, storiesHash, storyId } = state;
const { parameters } = storiesHash[storyId] || {};
return {
@ -25,13 +35,13 @@ const mapper = ({ api, state }) => {
description: getDescription(storiesHash, storyId),
...api.getUrlState(),
queryParams: customQueryParams,
docsOnly: parameters && parameters.docsOnly,
docsOnly: (parameters && parameters.docsOnly) as boolean,
location,
parameters,
};
};
function getBaseUrl() {
function getBaseUrl(): string {
try {
return PREVIEW_URL || 'iframe.html';
} catch (e) {
@ -41,7 +51,7 @@ function getBaseUrl() {
const PreviewConnected = React.memo(props => (
<Consumer filter={mapper}>
{fromState => (
{(fromState: ReturnType<typeof mapper>) => (
<Preview
{...props}
baseUrl={getBaseUrl()}