Add tour kit component (#33229)

* Add tour-kit component

* Add @types/wordpress__viewport to @woocommerce/components devDeps

* Add tour-kit README.md

* Add primaryButtonText option to tour kit step meta

* Add changelog

* Remove unneeded style import

* Set position and z-index style for tour-kit

* Add disable primary button feature for tour kit

* Export TourKitTypes

* Update style-build config for @automattic/* packages

* Add @automattic/* deps for components

* Ignore fs in webpack.config.js

* Update tour-kit stories

* Add tour-kit tests

* Update tour-kit README.md

* Update tour-kit types

* Update webpack.config.js

* Update style.scss

* Add auto focus feature

* Update type doc
This commit is contained in:
Chi-Hsuan Huang 2022-06-08 17:16:31 +08:00 committed by GitHub
parent be15a35038
commit 5db5c8b110
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 2516 additions and 934 deletions

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add tour kit component

View File

@ -26,7 +26,9 @@
],
"types": "build-types",
"dependencies": {
"@automattic/calypso-color-schemes": "^2.1.1",
"@automattic/interpolate-components": "^1.2.0",
"@automattic/tour-kit": "^1.0.0",
"@woocommerce/csv-export": "workspace:*",
"@woocommerce/currency": "workspace:*",
"@woocommerce/data": "workspace:*",
@ -93,6 +95,7 @@
"@testing-library/jest-dom": "^5.16.2",
"@testing-library/react": "^12.1.3",
"@testing-library/user-event": "^13.5.0",
"@types/wordpress__viewport": "^2.5.4",
"@woocommerce/eslint-plugin": "workspace:*",
"@woocommerce/internal-style-build": "workspace:*",
"@wordpress/browserslist-config": "^4.1.1",

View File

@ -58,3 +58,5 @@ export { default as ViewMoreList } from './view-more-list';
export { default as WebPreview } from './web-preview';
export { Badge } from './badge';
export { DynamicForm } from './dynamic-form';
export { default as TourKit } from './tour-kit';
export * as TourKitTypes from './tour-kit/types';

View File

@ -1,3 +1,8 @@
/**
* External Dependencies
*/
@import '@automattic/tour-kit/dist/esm/styles.scss';
/**
* Internal Dependencies
*/
@ -38,3 +43,4 @@
@import 'web-preview/style.scss';
@import 'badge/style.scss';
@import 'dynamic-form/style.scss';
@import 'tour-kit/style.scss';

View File

@ -0,0 +1,116 @@
# tour-kit
A Woocommerce Tour Kit variant based on [@automattic/tour-kit](https://github.com/Automattic/wp-calypso/blob/trunk/packages/tour-kit/README.md) for configurable guided tours. Contains some optional effects (like spotlight and overlay) that can be enabled/disabled depending on the desired use..
Uses [Popper.js](https://popper.js.org/) underneath (also customizable via tour configuration).
## Usage
A typical expected workflow builds around:
1. Define the criteria for showing a tour.
2. Define a configuration for the tour, passing along a handler for closing.
3. Render it (or not).
### Sample
```jsx
import { TourKit } from '@woocommerce/components';
function Tour() {
// 1. Define the criteria for showing a tour:
const [ showTour, setShowTour ] = useState( true );
// 2. Define a configuration for the tour, passing along a handler for closing.
const config = {
steps: [
{
referenceElements: {
desktop: '.render-step-near-me',
},
meta: {
heading: 'Lorem ipsum dolor sit amet.',
descriptions: {
desktop: 'Lorem ipsum dolor sit amet.',
},
primaryButtonText: "Done"
},
},
],
closeHandler: () => setShowTour( false ),
options: {},
};
// 3. Render it (or not):
if ( ! showTour ) {
return null;
}
return <TourKit config={ config } />;
}
```
## Accessibility
### Keyboard Navigation
When a tour is rendered and focused, the following functionality exists:
- Close the tour on `ESC` key (in minimized view)
- Go to previous/next step on `left/right` arrow keys (in step view)
## Configuration
The main API for configuring a tour is the config object. See example usage and [types.ts](./types.ts) for the full definition.
`config.steps`: An array of objects that define the content we wish to render on the page. Each step defined by:
- `referenceElements` (optional): A set of `desktop` & `mobile` selectors to render the step near.
- `focusElement` (optional): A set of `desktop` & `mobile` & `iframe` selectors to automatically focus.
- `meta`: Arbitrary object that encloses the content we want to render for each step.
- `classNames` (optional): An array or CSV of CSS classes applied to a step.
`config.closeHandler`: The callback responsible for closing the tour.
- `tourStep`: A React component that will be called to render each step. Receives the following properties:
- `steps`: The steps defined for the tour.
- `currentStepIndex`
- `onDismiss`: Handler that dismissed/closes the tour.
- `onNext`: Handler that progresses the tour to the next step.
- `onPrevious`: Handler that takes the tour to the previous step.
- `onMinimize`: Handler that minimizes the tour (passes rendering to `tourMinimized`).
- `setInitialFocusedElement`: A dispatcher that assigns an element to be initially focused when a step renders (see examples).
- `onGoToStep`: Handler that progresses the tour to a given step index.
- `tourMinimized`: A React component that will be called to render a minimized view for the tour. Receives the following properties:
- `steps`: The steps defined for the tour.
- `currentStepIndex`
- `onDismiss`: Handler that dismissed/closes the tour.
- `onMaximize`: Handler that expands the tour (passes rendering to `tourStep`).
`config.options` (optional):
- `classNames` (optional): An array or CSV of CSS classes to enclose the main tour frame with.
- `effects`: An object to enable/disable/combine various tour effects:
- `spotlight`: Adds a semi-transparent overlay and highlights the reference element when provided with a transparent box over it. Expects an object with optional styles to override the default highlight/spotlight behavior when provided (default: spotlight wraps the entire reference element).
- `interactivity`: An object that configures whether the user is allowed to interact with the referenced element during the tour
- `styles`: CSS properties that configures the styles applied to the spotlight overlay
- `arrowIndicator`: Adds an arrow tip pointing at the reference element when provided.
- `overlay`: Includes the semi-transparent overlay for all the steps (also blocks interactions with the rest of the page)
- `autoScroll`: The page scrolls up and down automatically such that the step target element is visible to the user.
- `callbacks`: An object of callbacks to handle side effects from various interactions (see [types.ts](./src/types.ts)).
- `popperModifiers`: The tour uses Popper to position steps near reference elements (and for other effects). An implementation can pass its own modifiers to tailor the functionality further e.g. more offset or padding from the reference element.
- `tourRating` (optional - only in WPCOM Tour Kit variant):
- `enabled`: Whether to show rating in last step.
- `useTourRating`: (optional) A hook to provide the rating from an external source/state (see [types.ts](./src/types.ts)).
- `onTourRate`: (optional) A callback to fire off when a rating is submitted.
- `portalElementId`: A string that lets you customize under which DOM element the Tour will be appended.
`placement` (Optional) : Describes the preferred placement of the popper. Possible values are left-start, left, left-end, top-start, top, top-end, right-start, right, right-end, bottom-start, bottom, and bottom-end.

View File

@ -0,0 +1,29 @@
/**
* External dependencies
*/
import { Button, Flex, Icon } from '@wordpress/components';
import { closeSmall } from '@wordpress/icons';
import { __ } from '@wordpress/i18n';
import { createElement } from '@wordpress/element';
import { TourStepRendererProps } from '@automattic/tour-kit';
interface Props {
onDismiss: TourStepRendererProps[ 'onDismiss' ];
}
const StepControls: React.FunctionComponent< Props > = ( { onDismiss } ) => {
return (
<Flex className="woocommerce-tour-kit-step-controls" justify="flex-end">
<Button
className="woocommerce-tour-kit-step-controls__close-btn"
label={ __( 'Close Tour', 'woocommerce' ) }
icon={ <Icon icon={ closeSmall } viewBox="6 4 12 14" /> }
iconSize={ 16 }
size={ 16 }
onClick={ onDismiss( 'close-btn' ) }
></Button>
</Flex>
);
};
export default StepControls;

View File

@ -0,0 +1,96 @@
/**
* External dependencies
*/
import { Button } from '@wordpress/components';
import { __, sprintf } from '@wordpress/i18n';
import { createElement } from '@wordpress/element';
/**
* Internal dependencies
*/
import type { WooTourStepRendererProps } from '../types';
type Props = Omit<
WooTourStepRendererProps,
'onMinimize' | 'setInitialFocusedElement'
>;
const StepNavigation: React.FunctionComponent< Props > = ( {
currentStepIndex,
onNextStep,
onPreviousStep,
onDismiss,
steps,
} ) => {
const isFirstStep = currentStepIndex === 0;
const isLastStep = currentStepIndex === steps.length - 1;
const { primaryButton = { text: '', isDisabled: false } } = steps[
currentStepIndex
].meta;
const NextButton = (
<Button
className="woocommerce-tour-kit-step-navigation__next-btn"
isPrimary
disabled={ primaryButton.isDisabled }
onClick={ onNextStep }
>
{ primaryButton.text || __( 'Next', 'woocommerce' ) }
</Button>
);
const BackButton = (
<Button
className="woocommerce-tour-kit-step-navigation__back-btn"
isSecondary
onClick={ onPreviousStep }
>
{ __( 'Back', 'woocommerce' ) }
</Button>
);
const renderButtons = () => {
if ( isFirstStep ) {
return <div>{ NextButton }</div>;
}
if ( isLastStep ) {
return (
<div>
{ BackButton }
<Button
isPrimary
disabled={ primaryButton.isDisabled }
className="woocommerce-tour-kit-step-navigation__done-btn"
onClick={ onDismiss( 'done-btn' ) }
>
{ primaryButton.text || __( 'Done', 'woocommerce' ) }
</Button>
</div>
);
}
return (
<div>
{ BackButton }
{ NextButton }
</div>
);
};
return (
<div className="woocommerce-tour-kit-step-navigation">
<div className="woocommerce-tour-kit-step-navigation__step">
{ sprintf(
/* translators: current progress in tour, eg: "Step 2 of 4" */
__( 'Step %1$d of %2$d', 'woocommerce' ),
currentStepIndex + 1,
steps.length
) }
</div>
{ renderButtons() }
</div>
);
};
export default StepNavigation;

View File

@ -0,0 +1,115 @@
/**
* External dependencies
*/
import { withViewportMatch } from '@wordpress/viewport';
import { Card, CardBody, CardFooter, CardHeader } from '@wordpress/components';
import { createElement, useEffect } from '@wordpress/element';
/**
* Internal dependencies
*/
import StepNavigation from './step-navigation';
import StepControls from './step-controls';
import type { WooTourStepRendererProps } from '../types';
const getFocusElement = (
focusElementSelector: string | null,
iframeSelector: string | null
) => {
if ( ! focusElementSelector ) {
return null;
}
if ( iframeSelector ) {
const iframeElement = document.querySelector< HTMLIFrameElement >(
iframeSelector
);
if ( ! iframeElement ) {
return null;
}
const innerDoc =
iframeElement.contentDocument ||
( iframeElement.contentWindow &&
iframeElement.contentWindow.document );
if ( ! innerDoc ) {
return null;
}
return innerDoc.querySelector< HTMLElement >( focusElementSelector );
}
return document.querySelector< HTMLElement >( focusElementSelector );
};
const Step: React.FunctionComponent<
WooTourStepRendererProps & {
isViewportMobile: boolean;
}
> = ( {
steps,
currentStepIndex,
onDismiss,
onNextStep,
onPreviousStep,
setInitialFocusedElement,
onGoToStep,
isViewportMobile,
} ) => {
const { descriptions, heading } = steps[ currentStepIndex ].meta;
const description =
descriptions[ isViewportMobile ? 'mobile' : 'desktop' ] ??
descriptions.desktop;
const focusElementSelector =
steps[ currentStepIndex ].focusElement?.[
isViewportMobile ? 'mobile' : 'desktop'
] || null;
const iframeSelector =
steps[ currentStepIndex ].focusElement?.iframe || null;
const focusElement = getFocusElement(
focusElementSelector,
iframeSelector
);
/*
* Focus the element when step renders.
*/
useEffect( () => {
if ( focusElement ) {
setInitialFocusedElement( focusElement );
}
}, [ focusElement, setInitialFocusedElement ] );
return (
<Card className="woocommerce-tour-kit-step" isElevated>
<CardHeader isBorderless={ true } size="small">
<StepControls onDismiss={ onDismiss } />
</CardHeader>
<CardBody className="woocommerce-tour-kit-step__body" size="small">
<h2 className="woocommerce-tour-kit-step__heading">
{ heading }
</h2>
<p className="woocommerce-tour-kit-step__description">
{ description }
</p>
</CardBody>
<CardFooter isBorderless={ true } size="small">
<StepNavigation
currentStepIndex={ currentStepIndex }
onGoToStep={ onGoToStep }
onNextStep={ onNextStep }
onPreviousStep={ onPreviousStep }
onDismiss={ onDismiss }
steps={ steps }
></StepNavigation>
</CardFooter>
</Card>
);
};
export default withViewportMatch( {
isViewportMobile: '< medium',
} )( Step );

View File

@ -0,0 +1,42 @@
/**
* External dependencies
*/
import { createElement } from '@wordpress/element';
import TourKit, { TourStepRenderer } from '@automattic/tour-kit';
/**
* Internal dependencies
*/
import TourKitStep from './components/step';
import type { WooConfig } from './types';
interface Props {
config: WooConfig;
}
const defaultOptions = {
effects: { spotlight: {}, arrowIndicator: true },
};
const WooTourKit: React.FunctionComponent< Props > = ( { config } ) => {
return (
<TourKit
__temp__className={ 'woocommerce-tour-kit' }
config={ {
options: {
...defaultOptions,
...config.options,
},
...config,
renderers: {
tourStep: TourKitStep as TourStepRenderer,
// Disable minimize feature for woo tour kit.
tourMinimized: () => null,
},
} }
/>
);
};
export default WooTourKit;

View File

@ -0,0 +1,212 @@
/**
* External dependencies
*/
import { createElement, useState } from '@wordpress/element';
/**
* Internal dependencies
*/
import './style.scss';
import '../style.scss';
import WooTourKit from '..';
import type { WooConfig, WooOptions } from '../types';
export default {
title: 'WooCommerce Admin/components/TourKit',
component: WooTourKit,
};
const References = () => {
return (
<div className={ 'storybook__tourkit-references' }>
<div className={ 'storybook__tourkit-references-container' }>
<div className={ 'storybook__tourkit-references-a' }>
<p>Reference A</p>
</div>
<div className={ 'storybook__tourkit-references-b' }>
<p>Reference B</p>
<div style={ { display: 'grid', placeItems: 'center' } }>
<input
style={ { margin: 'auto', display: 'block' } }
></input>
</div>
</div>
<div className={ 'storybook__tourkit-references-c' }>
<p>Reference C</p>
</div>
<div className={ 'storybook__tourkit-references-d' }>
<p>Reference D</p>
</div>
</div>
</div>
);
};
const Tour = ( {
onClose,
options,
placement,
}: {
onClose: () => void;
options?: WooOptions;
placement?: WooConfig[ 'placement' ];
} ) => {
const config: WooConfig = {
placement,
steps: [
{
referenceElements: {
desktop: '.storybook__tourkit-references-a',
mobile: '.storybook__tourkit-references-a',
},
meta: {
heading: 'Change content',
descriptions: {
desktop:
'You can change the content and add any relevant links.',
mobile:
'You can change the content and add any relevant links.',
},
},
},
{
referenceElements: {
desktop: '.storybook__tourkit-references-b',
mobile: '.storybook__tourkit-references-b',
},
focusElement: {
desktop: '.storybook__tourkit-references-b input',
},
meta: {
heading: 'Shipping zones',
descriptions: {
desktop:
'We added a few shipping zones for you based on your location, but you can manage them at any time.',
mobile:
'A shipping zone is a geographic area where a certain set of shipping methods are offered.',
},
},
},
{
referenceElements: {
desktop: '.storybook__tourkit-references-c',
mobile: '.storybook__tourkit-references-c',
},
meta: {
heading: 'Shipping methods',
descriptions: {
desktop:
'We defaulted to some recommended shipping methods based on your store location, but you can manage them at any time within each shipping zone settings. ',
mobile:
'We defaulted to some recommended shipping methods based on your store location, but you can manage them at any time within each shipping zone settings. ',
},
},
},
{
referenceElements: {
desktop: '.storybook__tourkit-references-d',
mobile: '.storybook__tourkit-references-d',
},
meta: {
heading: 'Laura 4',
descriptions: {
desktop: 'Lorem ipsum dolor sit amet.',
mobile: 'Lorem ipsum dolor sit amet.',
},
primaryButton: {
isDisabled: true,
text: 'Keep editing',
},
},
},
],
closeHandler: onClose,
options: {
classNames: [ 'mytour' ],
...options,
},
};
return <WooTourKit config={ config } />;
};
const StoryTour = ( {
options = {},
placement,
}: {
options?: WooConfig[ 'options' ];
placement?: WooConfig[ 'placement' ];
} ) => {
const [ showTour, setShowTour ] = useState( false );
return (
<div className="storybook__tourkit">
<References />
{ ! showTour && (
<button onClick={ () => setShowTour( true ) }>
Start Tour
</button>
) }
{ showTour && (
<Tour
placement={ placement }
onClose={ () => setShowTour( false ) }
options={ options }
/>
) }
</div>
);
};
export const NoEffects = () => (
<StoryTour
options={ {
effects: {},
} }
/>
);
export const Spotlight = () => (
<StoryTour
options={ {
effects: { arrowIndicator: true, spotlight: {} },
} }
/>
);
export const Overlay = () => (
<StoryTour
options={ {
effects: { arrowIndicator: true, overlay: true },
} }
/>
);
export const SpotlightInteractivity = () => (
<StoryTour
options={ {
effects: {
spotlight: {
interactivity: {
rootElementSelector: '#root',
enabled: true,
},
},
},
} }
/>
);
export const AutoScroll = () => (
<>
<div style={ { height: '10vh' } }></div>
<StoryTour
options={ {
effects: {
autoScroll: {
behavior: 'smooth',
},
},
} }
/>
</>
);
export const Placement = () => <StoryTour placement={ 'left' } />;

View File

@ -0,0 +1,73 @@
.storybook__tourkit {
height: 500px;
}
.storybook__tourkit-step,
.storybook__tourkit-minimized {
padding: 10px;
}
.storybook__tourkit-step {
min-height: 150px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.storybook__tourkit-step-controls,
.storybook__tourkit-minimized-controls {
display: flex;
flex-direction: row-reverse;
}
.storybook__tourkit-references {
height: 400px;
margin-bottom: 30px;
overflow: auto;
resize: auto;
.storybook__tourkit-references-container {
height: 100%;
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr 1fr;
grid-template-rows: 1fr 1fr 1fr 1fr;
gap: 0 0;
grid-auto-flow: row;
grid-template-areas:
'Ref-A Ref-B Ref-B Ref-B Ref-D'
'Ref-A Ref-B Ref-B Ref-B Ref-D'
'Ref-A Ref-C Ref-C Ref-C Ref-D'
'Ref-A Ref-C Ref-C Ref-C Ref-D';
}
.storybook__tourkit-references-a,
.storybook__tourkit-references-b,
.storybook__tourkit-references-c,
.storybook__tourkit-references-d {
display: grid;
> p {
align-self: center;
justify-self: center;
}
}
.storybook__tourkit-references-a {
background-color: silver;
grid-area: Ref-A;
}
.storybook__tourkit-references-b {
background-color: orange;
grid-area: Ref-B;
}
.storybook__tourkit-references-c {
background-color: palevioletred;
grid-area: Ref-C;
}
.storybook__tourkit-references-d {
background-color: gainsboro;
grid-area: Ref-D;
}
}

View File

@ -0,0 +1,66 @@
.woocommerce-tour-kit{
box-shadow: 0 0 8px rgba( 0, 0, 0, 0.15 );
position: absolute;
z-index: 9999;
.woocommerce-tour-kit-step {
width: 350px;
padding-bottom: 4px;
}
.woocommerce-tour-kit-step__body {
padding-top: 0;
padding-bottom: 8px;
}
.woocommerce-tour-kit-step__heading {
color: $studio-gray-100;
font-weight: 500;
font-size: 16px;
margin: 0;
letter-spacing: -0.1px;
line-height: 24px;
}
.woocommerce-tour-kit-step__description {
color: $studio-gray-80;
font-style: normal;
font-weight: 400;
font-size: 14px;
line-height: 20px;
letter-spacing: -0.24px;
margin: 4px 0 0;
}
.woocommerce-tour-kit-step-controls__close-btn {
height: 16px;
min-width: 16px;
padding: 0;
svg {
fill: #1e1e1e;
}
}
.woocommerce-tour-kit-step-navigation {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
button {
margin-left: $gap;
}
}
.woocommerce-tour-kit-step-navigation__step {
color: $studio-gray-60;
font-style: normal;
font-weight: 400;
font-size: 12px;
line-height: 20px;
}
.tour-kit-frame__arrow, .tour-kit-frame__arrow::before {
z-index: 1;
}
}

View File

@ -0,0 +1,61 @@
/**
* External dependencies
*/
import { render, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { createElement } from '@wordpress/element';
/**
* Internal dependencies
*/
import TourKit from '..';
jest.mock( '@automattic/calypso-config' );
const config = {
steps: [
{
referenceElements: {
desktop: '.render-step-near-me',
},
meta: {
heading: 'Step1',
descriptions: {
desktop: 'Description',
},
},
},
{
referenceElements: {
desktop: '.render-step-near-me',
},
meta: {
heading: 'Step2',
descriptions: {
desktop: 'Description',
},
},
},
],
closeHandler: () => jest.fn(),
options: {},
};
describe( 'TourKit', () => {
it( 'should render TourKit', () => {
const { queryByText } = render( <TourKit config={ config } /> );
expect( queryByText( 'Step1' ) ).toBeInTheDocument();
} );
it( 'should go to next step and show a back button', async () => {
const { queryByText, getByRole } = render(
<TourKit config={ config } />
);
userEvent.click( getByRole( 'button', { name: 'Next' } ) );
await waitFor( () =>
expect( queryByText( 'Step2' ) ).toBeInTheDocument()
);
expect( getByRole( 'button', { name: 'Back' } ) ).toBeInTheDocument();
} );
} );

View File

@ -0,0 +1,42 @@
/**
* External dependencies
*/
import {
Step,
Options,
Config,
TourStepRendererProps,
} from '@automattic/tour-kit';
export interface WooStep extends Step {
meta: {
heading: string | null;
descriptions: {
desktop: string | React.ReactElement | null;
mobile?: string | React.ReactElement | null;
};
primaryButton?: {
/** Set a text for the button. Default to "Done" for the last step and "Next" for the other steps */
text?: string;
/** Disable the button or not. Default to False */
isDisabled?: boolean;
};
};
/** Auto apply the focus state for the element. Default to null */
focusElement?: {
desktop?: string;
mobile?: string;
/** Iframe HTML selector. Default to null. If set, it will find the focus element in the iframe */
iframe?: string;
};
}
export type WooOptions = Options;
export interface WooConfig extends Omit< Config, 'renderers' | 'isMinimized' > {
steps: WooStep[];
options?: WooOptions;
}
export interface WooTourStepRendererProps extends TourStepRendererProps {
steps: WooStep[];
}

View File

@ -34,12 +34,26 @@ module.exports = {
],
},
webpackImporter: true,
additionalData:
'@use "sass:math";' +
'@import "_colors"; ' +
'@import "_variables"; ' +
'@import "_breakpoints"; ' +
'@import "_mixins"; ',
additionalData: ( content, loaderContext ) => {
const { resourcePath } = loaderContext;
if ( resourcePath.includes( '@automattic+' ) ) {
/*
* Skip adding additional data for @automattic/* packages to
* fix "SassError: @use rules must be written before any other rules."
* @automattic/* packages have included '@use "sass:math" and other necessary imports.
*/
return content;
}
return (
'@use "sass:math";' +
'@import "_colors"; ' +
'@import "_variables"; ' +
'@import "_breakpoints"; ' +
'@import "_mixins"; ' +
content
);
},
},
},
],

View File

@ -138,6 +138,9 @@ const webpackConfig = {
// Reduce bundle size by omitting Node crypto library.
// See https://github.com/woocommerce/woocommerce-admin/pull/5768
crypto: 'empty',
// Ignore fs, path to skip resolve errors for @automattic/calypso-config
fs: false,
path: false,
},
extensions: [ '.json', '.js', '.jsx', '.ts', '.tsx' ],
alias: {

File diff suppressed because it is too large Load Diff

View File

@ -77,5 +77,11 @@ module.exports = ( storybookConfig ) => {
} )
);
storybookConfig.resolve.fallback = {
...storybookConfig.resolve.fallback,
// Ignore fs to fix resolve 'fs' error for @automattic/calypso-config
fs: false,
};
return storybookConfig;
};