mirror of
https://github.com/storybookjs/storybook.git
synced 2025-04-01 05:05:25 +08:00
Merge pull request #16481 from storybookjs/feature/interactions-feature-flag
Interactions: move step debugger behind a feature flag
This commit is contained in:
commit
e260ec5f38
88
addons/interactions/src/Panel.stories.tsx
Normal file
88
addons/interactions/src/Panel.stories.tsx
Normal file
@ -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: [],
|
||||
},
|
||||
};
|
@ -6,14 +6,38 @@ 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];
|
||||
const completedStates = [CallStates.DONE, CallStates.ERROR];
|
||||
|
||||
@ -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>();
|
||||
@ -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';
|
||||
|
||||
@ -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),
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -65,11 +65,13 @@ export const Interaction = ({
|
||||
callsById,
|
||||
onClick,
|
||||
isDisabled,
|
||||
isDebuggingEnabled,
|
||||
}: {
|
||||
call: Call;
|
||||
callsById: Map<Call['id'], Call>;
|
||||
onClick: React.MouseEventHandler<HTMLElement>;
|
||||
isDisabled: boolean;
|
||||
isDebuggingEnabled?: boolean;
|
||||
}) => {
|
||||
const [isHovered, setIsHovered] = React.useState(false);
|
||||
return (
|
||||
@ -77,9 +79,9 @@ export const Interaction = ({
|
||||
<RowLabel
|
||||
call={call}
|
||||
onClick={onClick}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
disabled={isDisabled}
|
||||
disabled={isDebuggingEnabled ? isDisabled : true}
|
||||
onMouseEnter={() => isDebuggingEnabled && setIsHovered(true)}
|
||||
onMouseLeave={() => isDebuggingEnabled && setIsHovered(false)}
|
||||
>
|
||||
<StatusIcon status={isHovered ? CallStates.ACTIVE : call.state} />
|
||||
<MethodCallWrapper style={{ marginLeft: 6, marginBottom: 1 }}>
|
||||
|
@ -22,7 +22,7 @@ export default {
|
||||
),
|
||||
],
|
||||
parameters: {
|
||||
layout: 'fullscren',
|
||||
layout: 'fullscreen',
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -23,7 +23,7 @@ export default {
|
||||
),
|
||||
],
|
||||
parameters: {
|
||||
layout: 'fullscren',
|
||||
layout: 'fullscreen',
|
||||
},
|
||||
};
|
||||
|
||||
|
30
addons/interactions/src/mocks/index.ts
Normal file
30
addons/interactions/src/mocks/index.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { CallStates, Call } from '@storybook/instrumenter';
|
||||
|
||||
export const getCall = (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 };
|
||||
};
|
@ -38,6 +38,7 @@ const config: StorybookConfig = {
|
||||
logLevel: 'debug',
|
||||
features: {
|
||||
modernInlineRender: true,
|
||||
interactionsDebugger: true,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -154,6 +154,7 @@ export const parameters = {
|
||||
restoreScroll: true,
|
||||
},
|
||||
},
|
||||
actions: { argTypesRegex: '^on.*' },
|
||||
options: {
|
||||
storySort: (a, b) =>
|
||||
a[1].kind === b[1].kind ? 0 : a[1].id.localeCompare(b[1].id, undefined, { numeric: true }),
|
||||
|
@ -314,6 +314,11 @@ export interface StorybookConfig {
|
||||
*/
|
||||
breakingChangesV7?: boolean;
|
||||
|
||||
/**
|
||||
* Enable the step debugger functionality in Addon-interactions.
|
||||
*/
|
||||
interactionsDebugger?: boolean;
|
||||
|
||||
/**
|
||||
* Use Storybook 7.0 babel config scheme
|
||||
*/
|
||||
|
Loading…
x
Reference in New Issue
Block a user