Skip to content

Commit

Permalink
Merge pull request #5 from storybookjs/feat/subnavigation
Browse files Browse the repository at this point in the history
feat: subnav
  • Loading branch information
darleendenno authored Sep 8, 2021
2 parents a6aa616 + 6e74753 commit 8efa4c1
Show file tree
Hide file tree
Showing 7 changed files with 241 additions and 75 deletions.
2 changes: 1 addition & 1 deletion addons/interactions/.storybook/manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ import { addons } from '@storybook/addons';
import { themes } from '@storybook/theming';

addons.setConfig({
theme: themes.normal,
theme: themes.light,
});
42 changes: 20 additions & 22 deletions addons/interactions/src/Panel.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import global from 'global';
import React from 'react';
import ReactDOM from 'react-dom';
import { useChannel } from '@storybook/api';
import { AddonPanel, Button, Icons } from '@storybook/components';
import { useChannel, useParameter } from '@storybook/api';
import { AddonPanel, Icons } from '@storybook/components';
import { styled } from '@storybook/theming';

import { EVENTS } from './constants';
import { Call, CallRef, CallState } from './types';
import { MatcherResult } from './components/MatcherResult';
import { MethodCall } from './components/MethodCall';
import { StatusIcon } from './components/StatusIcon/StatusIcon';
import { Subnav } from './components/Subnav/Subnav';

interface PanelProps {
active: boolean;
Expand Down Expand Up @@ -40,20 +41,21 @@ const Interaction = ({
callsById: Record<Call['id'], Call>;
onClick: React.MouseEventHandler<HTMLElement>;
}) => {
const RowContainer = styled.div({
const RowContainer = styled.div(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
background: call.state === CallState.ERROR ? '#FFF5CF' : 'transparent',
borderBottom: '1px solid #eee',
borderBottom: `1px solid ${theme.color.mediumlight}`,
fontFamily: 'Monaco, monospace',
fontSize: 12,
});
}));

const RowLabel = styled.div({
display: 'grid',
gridTemplateColumns: '15px 1fr',
alignItems: 'center',
padding: '8px 10px',
minHeight: 40,
padding: '8px 15px',
cursor: call.state === CallState.ERROR ? 'default' : 'pointer',
opacity: call.state === CallState.PENDING ? 0.4 : 1,
'&:hover': {
Expand Down Expand Up @@ -190,6 +192,8 @@ export const Panel: React.FC<PanelProps> = (props) => {
},
});

const [fileName] = useParameter('fileName', '').split('/').slice(-1);

const { log, interactions, callsById, isDebugging } = state;
const hasException = interactions.some((call) => call.state === CallState.ERROR);
const hasPrevious = interactions.some((call) => call.state !== CallState.PENDING);
Expand Down Expand Up @@ -239,25 +243,19 @@ export const Panel: React.FC<PanelProps> = (props) => {
return (
<AddonPanel {...props}>
{tabButton && showStatus && ReactDOM.createPortal(statusIcon, tabButton)}

<Subnav
status={hasException ? CallState.ERROR : CallState.DONE}
storyFileName={fileName}
onPrevious={prev}
onNext={next}
onReplay={stop}
goToEnd={stop}
hasPrevious={hasPrevious}
hasNext={hasNext}
/>
{interactions.map((call) => (
<Interaction call={call} callsById={callsById} key={call.id} onClick={() => goto(call)} />
))}

<div style={{ padding: 3 }}>
<Button outline containsIcon title="Start debugging" onClick={start}>
<Icons icon="undo" />
</Button>
<Button outline containsIcon title="Step back" onClick={prev} disabled={!hasPrevious}>
<Icons icon="arrowleftalt" />
</Button>
<Button outline containsIcon title="Step over" onClick={next} disabled={!hasNext}>
<Icons icon="arrowrightalt" />
</Button>
<Button outline containsIcon title="Play" onClick={stop} disabled={!hasNext}>
<Icons icon="play" />
</Button>
</div>
</AddonPanel>
);
};
18 changes: 6 additions & 12 deletions addons/interactions/src/components/StatusBadge/StatusBadge.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,27 @@
import React from 'react';
import { styled, typography } from '@storybook/theming';
import { CallState } from '../../types';
import { theme } from '../../theme';

export interface StatusBadgeProps {
status: `${CallState}`;
}
const {
colors: {
pure: { green, red, ochre },
},
} = theme;

const StyledBadge = styled.div(({ status }: StatusBadgeProps) => {
const StyledBadge = styled.div<StatusBadgeProps>(({ theme, status }) => {
const backgroundColor = {
[CallState.DONE]: green,
[CallState.ERROR]: red,
[CallState.PENDING]: ochre,
[CallState.DONE]: theme.color.positive,
[CallState.ERROR]: theme.color.negative,
[CallState.PENDING]: theme.color.warning,
}[status];
return {
padding: '4px 8px',
padding: '4px 6px 4px 8px;',
borderRadius: '4px',
backgroundColor: backgroundColor,
color: 'white',
fontFamily: typography.fonts.base,
textTransform: 'uppercase',
fontSize: typography.size.s1,
letterSpacing: 3,
fontWeight: 500,
fontWeight: typography.weight.bold,
width: 65,
textAlign: 'center',
};
Expand Down
41 changes: 19 additions & 22 deletions addons/interactions/src/components/StatusIcon/StatusIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,32 +10,29 @@ export interface StatusIconProps extends IconsProps {

const {
colors: {
pure: { green, red, gray },
pure: { gray },
},
} = theme;

const StyledStatusIcon = styled(Icons)(({ status }: StatusIconProps) => ({
width: status === CallState.PENDING ? 6 : 12,
height: status === CallState.PENDING ? 6 : 12,
color: status === CallState.PENDING ? gray[500] : status === CallState.DONE ? green : red,
justifySelf: 'center',
}));
const StyledStatusIcon = styled(Icons)<StatusIconProps>(({ theme, status }) => {
const color = {
[CallState.PENDING]: gray[500],
[CallState.DONE]: theme.color.positive,
[CallState.ERROR]: theme.color.negative,
}[status];
return {
width: status === CallState.PENDING ? 6 : 12,
height: status === CallState.PENDING ? 6 : 12,
color,
justifySelf: 'center',
};
});

export const StatusIcon: React.FC<StatusIconProps> = ({ status }) => {
// TODO: update when stop icon is added to design library
const icon = status === CallState.PENDING ? 'circle' : 'check';
if (status === CallState.ERROR)
return (
<span
style={{
display: 'block',
width: 10,
height: 10,
background: red,
borderRadius: 1,
justifySelf: 'center',
}}
/>
);
const icon = {
[CallState.DONE]: 'check',
[CallState.PENDING]: 'circle',
[CallState.ERROR]: 'stopalt',
}[status] as IconsProps['icon'];
return <StyledStatusIcon status={status} icon={icon} />;
};
50 changes: 50 additions & 0 deletions addons/interactions/src/components/Subnav/Subnav.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { CallState } from '../../types';
import { Subnav } from './Subnav';

export default {
title: 'Subnav',
component: Subnav,
args: {
onPrevious: () => {},
onNext: () => {},
onReplay: () => {},
goToEnd: () => {},
storyFileName: 'Subnav.stories.tsx',
hasNext: true,
hasPrevious: true,
},
};

export const Pass = {
args: {
status: CallState.DONE,
},
};

export const Fail = {
args: {
status: CallState.ERROR,
},
};

export const Runs = {
args: {
status: CallState.PENDING,
},
};

export const AtTheBeginning = {
name: 'at the beginning',
args: {
status: CallState.DONE,
hasPrevious: false,
},
};

export const AtTheEnd = {
name: 'at the end',
args: {
status: CallState.DONE,
hasNext: false,
},
};
144 changes: 144 additions & 0 deletions addons/interactions/src/components/Subnav/Subnav.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import React, { useState } from 'react';
import { Button, Icons, Separator, P } from '@storybook/components';
import { styled } from '@storybook/theming';
import { CallState } from '../../types';
import { StatusBadge } from '../StatusBadge/StatusBadge';
import { transparentize } from 'polished';
import { ButtonProps } from '@storybook/components/dist/ts3.9/Button/Button';

const StyledSubnav = styled.nav(({ theme }) => ({
background: theme.background.app,
borderBottom: `1px solid ${theme.color.border}`,
height: 40,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
paddingLeft: 15,
position: 'sticky',
top: 0,
zIndex: 1,
}));

export interface SubnavProps {
status: `${CallState}`;
onPrevious: () => void;
onNext: () => void;
onReplay: () => void;
goToEnd: () => void;
storyFileName?: string;
hasPrevious: boolean;
hasNext: boolean;
}

const StyledButton = styled(Button)(({ theme }) => ({
borderRadius: 4,
padding: 6,
color: theme.color.dark,
'&:hover,&:focus-visible': {
color: theme.color.secondary,
},
}));

const StyledIconButton = styled(StyledButton)(({ theme }) => ({
color: theme.color.mediumdark,
margin: '0 3px',
'&:hover,&:focus-visible': {
background: transparentize(0.9, theme.color.secondary),
},
}));

interface AnimatedButtonProps extends ButtonProps {
animating?: boolean;
}
const StyledAnimatedIconButton = styled(StyledIconButton)<AnimatedButtonProps>(
({ theme, animating }) => ({
svg: {
animation: animating && `${theme.animation.rotate360} 1000ms ease-out`,
},
})
);

const StyledSeparator = styled(Separator)({
marginTop: 0,
});

const StyledLocation = styled(P)(({ theme }) => ({
color: theme.color.dark,
justifyContent: 'flex-end',
textAlign: 'right',
paddingRight: 15,
fontSize: 13,
}));

const Group = styled.div({
display: 'flex',
alignItems: 'center',
});

const PlaybackButton = styled(StyledIconButton)({
marginLeft: 9,
});

const JumpToEndButton = styled(StyledButton)({
marginLeft: 9,
marginRight: 9,
});

export const Subnav: React.FC<SubnavProps> = ({
status,
onPrevious,
onNext,
onReplay,
goToEnd,
storyFileName,
hasNext,
hasPrevious,
}) => {
const buttonText = status === CallState.ERROR ? 'Jump to error' : 'Jump to end';
const [isAnimating, setIsAnimating] = useState(false);
const animateAndReplay = () => {
setIsAnimating(true);
onReplay();
};

return (
<StyledSubnav>
<Group>
<StatusBadge status={status} />

<JumpToEndButton onClick={goToEnd} disabled={!hasNext}>
{buttonText}
</JumpToEndButton>

<StyledSeparator />

<PlaybackButton
containsIcon
title="Previous step"
onClick={onPrevious}
disabled={!hasPrevious}
>
<Icons icon="playback" />
</PlaybackButton>
<StyledIconButton containsIcon title="Next step" onClick={onNext} disabled={!hasNext}>
<Icons icon="playnext" />
</StyledIconButton>
<StyledAnimatedIconButton
containsIcon
title="Replay interactions"
onClick={animateAndReplay}
onAnimationEnd={() => setIsAnimating(false)}
animating={isAnimating}
data-test-id="button--replay"
>
<Icons icon="sync" />
</StyledAnimatedIconButton>
</Group>
{storyFileName && (
<Group>
<StyledLocation>{storyFileName}</StyledLocation>
</Group>
)}
</StyledSubnav>
);
};
Loading

0 comments on commit 8efa4c1

Please sign in to comment.