Skip to content

Commit

Permalink
Merge pull request storybookjs#16481 from storybookjs/feature/interac…
Browse files Browse the repository at this point in the history
…tions-feature-flag

Interactions: move step debugger behind a feature flag
  • Loading branch information
yannbf committed Oct 27, 2021
2 parents eff5efe + 65d5f62 commit e260ec5
Show file tree
Hide file tree
Showing 10 changed files with 272 additions and 84 deletions.
88 changes: 88 additions & 0 deletions addons/interactions/src/Panel.stories.tsx
@@ -0,0 +1,88 @@
import React from 'react';
import { ComponentStoryObj, ComponentMeta } from '@storybook/react';
import { CallStates } from '@storybook/instrumenter';
import { styled } from '@storybook/theming';

import { getCall } from './mocks';
import { AddonPanelPure } from './Panel';

const StyledWrapper = styled.div(({ theme }) => ({
backgroundColor: theme.background.content,
color: theme.color.defaultText,
display: 'block',
height: '100%',
position: 'absolute',
left: 0,
right: 0,
bottom: 0,
overflow: 'auto',
}));

export default {
title: 'Addons/Interactions/Panel',
component: AddonPanelPure,
decorators: [
(Story: any) => (
<StyledWrapper id="panel-tab-content">
<Story />
</StyledWrapper>
),
],
parameters: {
layout: 'fullscreen',
},
args: {
calls: new Map(),
endRef: null,
fileName: 'addon-interactions.stories.tsx',
hasException: false,
hasNext: false,
hasPrevious: true,
interactions: [getCall(CallStates.DONE)],
isDisabled: false,
isPlaying: false,
showTabIcon: false,
isDebuggingEnabled: true,
// prop for the AddonPanel used as wrapper of Panel
active: true,
},
} as ComponentMeta<typeof AddonPanelPure>;

type Story = ComponentStoryObj<typeof AddonPanelPure>;

export const Passing: Story = {
args: {
interactions: [getCall(CallStates.DONE)],
},
};

export const Paused: Story = {
args: {
isPlaying: true,
interactions: [getCall(CallStates.WAITING)],
},
};

export const Playing: Story = {
args: {
isPlaying: true,
interactions: [getCall(CallStates.ACTIVE)],
},
};

export const Failed: Story = {
args: {
hasException: true,
interactions: [getCall(CallStates.ERROR)],
},
};

export const WithDebuggingDisabled: Story = {
args: { isDebuggingEnabled: false },
};

export const NoInteractions: Story = {
args: {
interactions: [],
},
};
178 changes: 133 additions & 45 deletions addons/interactions/src/Panel.tsx
Expand Up @@ -6,12 +6,36 @@ import { STORY_RENDER_PHASE_CHANGED } from '@storybook/core-events';
import { AddonPanel, Link, Placeholder } from '@storybook/components';
import { EVENTS, Call, CallStates, LogItem } from '@storybook/instrumenter';
import { styled } from '@storybook/theming';

import { StatusIcon } from './components/StatusIcon/StatusIcon';
import { Subnav } from './components/Subnav/Subnav';
import { Interaction } from './components/Interaction/Interaction';

interface PanelProps {
const { FEATURES } = global;

interface AddonPanelProps {
active: boolean;
}

interface InteractionsPanelProps {
active: boolean;
showTabIcon?: boolean;
interactions: (Call & { state?: CallStates })[];
isDisabled?: boolean;
hasPrevious?: boolean;
hasNext?: boolean;
fileName?: string;
hasException?: boolean;
isPlaying?: boolean;
calls: Map<string, any>;
endRef?: React.Ref<HTMLDivElement>;
isDebuggingEnabled?: boolean;
onStart?: () => void;
onPrevious?: () => void;
onNext?: () => void;
onEnd?: () => void;
onScrollToEnd?: () => void;
onInteractionClick?: (callId: string) => void;
}

const pendingStates = [CallStates.ACTIVE, CallStates.WAITING];
Expand All @@ -21,7 +45,80 @@ const TabIcon = styled(StatusIcon)({
marginLeft: 5,
});

export const Panel: React.FC<PanelProps> = (props) => {
export const AddonPanelPure: React.FC<InteractionsPanelProps> = React.memo(
({
showTabIcon,
interactions,
isDisabled,
hasPrevious,
hasNext,
fileName,
hasException,
isPlaying,
onStart,
onPrevious,
onNext,
onEnd,
onScrollToEnd,
calls,
onInteractionClick,
endRef,
isDebuggingEnabled,
...panelProps
}) => {
return (
<AddonPanel {...panelProps}>
{showTabIcon &&
ReactDOM.createPortal(
<TabIcon status={hasException ? CallStates.ERROR : CallStates.ACTIVE} />,
global.document.getElementById('tabbutton-interactions')
)}
{isDebuggingEnabled && interactions.length > 0 && (
<Subnav
isDisabled={isDisabled}
hasPrevious={hasPrevious}
hasNext={hasNext}
storyFileName={fileName}
status={
// eslint-disable-next-line no-nested-ternary
isPlaying ? CallStates.ACTIVE : hasException ? CallStates.ERROR : CallStates.DONE
}
onStart={onStart}
onPrevious={onPrevious}
onNext={onNext}
onEnd={onEnd}
onScrollToEnd={onScrollToEnd}
/>
)}
{interactions.map((call) => (
<Interaction
key={call.id}
call={call}
callsById={calls}
isDebuggingEnabled={isDebuggingEnabled}
isDisabled={isDisabled}
onClick={() => onInteractionClick(call.id)}
/>
))}
<div ref={endRef} />
{!isPlaying && interactions.length === 0 && (
<Placeholder>
No interactions found
<Link
href="https://storybook.js.org/docs/react/essentials/interactions"
target="_blank"
withArrow
>
Learn how to add interactions to your story
</Link>
</Placeholder>
)}
</AddonPanel>
);
}
);

export const Panel: React.FC<AddonPanelProps> = (props) => {
const [isLocked, setLock] = React.useState(false);
const [isPlaying, setPlaying] = React.useState(true);
const [scrollTarget, setScrollTarget] = React.useState<HTMLElement>();
Expand Down Expand Up @@ -57,57 +154,48 @@ export const Panel: React.FC<PanelProps> = (props) => {
const [fileName] = storyFilePath.toString().split('/').slice(-1);
const scrollToTarget = () => scrollTarget?.scrollIntoView({ behavior: 'smooth', block: 'end' });

const isDebuggingEnabled = FEATURES.interactionsDebugger === true;

const isDebugging = log.some((item) => pendingStates.includes(item.state));
const hasPrevious = log.some((item) => completedStates.includes(item.state));
const hasNext = log.some((item) => item.state === CallStates.WAITING);
const hasActive = log.some((item) => item.state === CallStates.ACTIVE);
const hasException = log.some((item) => item.state === CallStates.ERROR);
const isDisabled = hasActive || isLocked || (isPlaying && !isDebugging);
const isDisabled = isDebuggingEnabled
? hasActive || isLocked || (isPlaying && !isDebugging)
: true;

const tabButton = global.document.getElementById('tabbutton-interactions');
const tabStatus = hasException ? CallStates.ERROR : CallStates.ACTIVE;
const showTabIcon = isDebugging || (!isPlaying && hasException);

const onStart = React.useCallback(() => emit(EVENTS.START, { storyId }), [storyId]);
const onPrevious = React.useCallback(() => emit(EVENTS.BACK, { storyId }), [storyId]);
const onNext = React.useCallback(() => emit(EVENTS.NEXT, { storyId }), [storyId]);
const onEnd = React.useCallback(() => emit(EVENTS.END, { storyId }), [storyId]);
const onInteractionClick = React.useCallback(
(callId: string) => emit(EVENTS.GOTO, { storyId, callId }),
[storyId]
);

return (
<AddonPanel {...props}>
{tabButton && showTabIcon && ReactDOM.createPortal(<TabIcon status={tabStatus} />, tabButton)}
{interactions.length > 0 && (
<Subnav
isDisabled={isDisabled}
hasPrevious={hasPrevious}
hasNext={hasNext}
storyFileName={fileName}
// eslint-disable-next-line no-nested-ternary
status={isPlaying ? CallStates.ACTIVE : hasException ? CallStates.ERROR : CallStates.DONE}
onStart={() => emit(EVENTS.START, { storyId })}
onPrevious={() => emit(EVENTS.BACK, { storyId })}
onNext={() => emit(EVENTS.NEXT, { storyId })}
onEnd={() => emit(EVENTS.END, { storyId })}
onScrollToEnd={scrollTarget && scrollToTarget}
/>
)}
{interactions.map((call) => (
<Interaction
key={call.id}
call={call}
callsById={calls.current}
onClick={() => emit(EVENTS.GOTO, { storyId, callId: call.id })}
isDisabled={isDisabled}
/>
))}
<div ref={endRef} />
{!isPlaying && interactions.length === 0 && (
<Placeholder>
No interactions found
<Link
href="https://storybook.js.org/docs/react/essentials/interactions"
target="_blank"
withArrow
>
Learn how to add interactions to your story
</Link>
</Placeholder>
)}
</AddonPanel>
<AddonPanelPure
showTabIcon={showTabIcon}
interactions={interactions}
isDisabled={isDisabled}
hasPrevious={hasPrevious}
hasNext={hasNext}
fileName={fileName}
hasException={hasException}
isPlaying={isPlaying}
calls={calls.current}
endRef={endRef}
isDebuggingEnabled={isDebuggingEnabled}
onStart={onStart}
onPrevious={onPrevious}
onNext={onNext}
onEnd={onEnd}
onInteractionClick={onInteractionClick}
onScrollToEnd={scrollTarget && scrollToTarget}
{...props}
/>
);
};
@@ -1,7 +1,8 @@
import { ComponentStoryObj, ComponentMeta } from '@storybook/react';
import { expect } from '@storybook/jest';
import { Call, CallStates } from '@storybook/instrumenter';
import { CallStates } from '@storybook/instrumenter';
import { userEvent, within } from '@storybook/testing-library';
import { getCall } from '../../mocks';

import { Interaction } from './Interaction';

Expand All @@ -13,59 +14,31 @@ export default {
args: {
callsById: new Map(),
isDisabled: false,
isDebuggingEnabled: true,
},
} as ComponentMeta<typeof Interaction>;

const getCallMock = (state: CallStates): Call => {
const defaultData = {
id: 'addons-interactions-accountform--standard-email-filled [3] change',
path: ['fireEvent'],
method: 'change',
storyId: 'addons-interactions-accountform--standard-email-filled',
args: [
{
__callId__: 'addons-interactions-accountform--standard-email-filled [2] getByTestId',
retain: false,
},
{
target: {
value: 'michael@chromatic.com',
},
},
],
interceptable: true,
retain: false,
state,
};

const overrides = CallStates.ERROR
? { exception: { callId: '', stack: '', message: "Things didn't work!" } }
: {};

return { ...defaultData, ...overrides };
};

export const Active: Story = {
args: {
call: getCallMock(CallStates.ACTIVE),
call: getCall(CallStates.ACTIVE),
},
};

export const Waiting: Story = {
args: {
call: getCallMock(CallStates.WAITING),
call: getCall(CallStates.WAITING),
},
};

export const Failed: Story = {
args: {
call: getCallMock(CallStates.ERROR),
call: getCall(CallStates.ERROR),
},
};

export const Done: Story = {
args: {
call: getCallMock(CallStates.DONE),
call: getCall(CallStates.DONE),
},
};

Expand Down

0 comments on commit e260ec5

Please sign in to comment.