dev: refactor core profiler loader (#39735)

* dev: refactor core profiler loader

* dev: added storybook example for loader

* lint

* Apply suggestions from code review

Co-authored-by: Ilyas Foo <foo.ilyas@gmail.com>

* addressed review feedback

---------

Co-authored-by: Ilyas Foo <foo.ilyas@gmail.com>
This commit is contained in:
RJ 2023-08-22 19:58:33 +10:00 committed by GitHub
parent 4876ab35b8
commit cb2cf79342
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 1508 additions and 202 deletions

View File

@ -0,0 +1,4 @@
Significance: minor
Type: dev
Refactored core profiler loader to be more generalizable and moved to @woocommerce/onboarding

View File

@ -36,13 +36,17 @@
"@wordpress/components": "wp-6.0",
"@wordpress/element": "wp-6.0",
"@wordpress/i18n": "wp-6.0",
"classnames": "^2.3.1",
"gridicons": "^3.4.0",
"react": "^17.0.2",
"string-similarity": "4.0.4"
},
"devDependencies": {
"@babel/core": "^7.17.5",
"@storybook/addon-knobs": "^7.0.2",
"@testing-library/react": "^12.1.3",
"@types/jest": "^27.4.1",
"@types/react": "^17.0.2",
"@types/string-similarity": "4.0.0",
"@types/wordpress__components": "^19.10.3",
"@types/wordpress__data": "^6.0.0",

View File

@ -0,0 +1,62 @@
.woocommerce-onboarding-progress-bar__container {
height: 8px;
width: 100%;
}
// Min width equal to height. This means small values look like each other, but all bars have a consistent radius.
.woocommerce-onboarding-progress-bar__filler {
height: 100%;
min-width: 8px;
}
// Loader page
.woocommerce-onboarding-loader {
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
background-color: white;
@include breakpoint( '<782px' ) {
padding: 0 20px;
}
.woocommerce-onboarding-loader__title {
padding: 0;
margin: 58px 0 0 0;
font-size: 28px;
font-weight: 500;
@include breakpoint( '<782px' ) {
font-size: 20px;
}
}
.woocommerce-onboarding-loader-wrapper {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
max-width: 520px;
}
.woocommerce-onboarding-progress-bar {
width: 520px;
margin: 16px 0 16px 0;
@include breakpoint( '<782px' ) {
width: 100%;
}
}
.woocommerce-onboarding-progress-bar__container {
height: 4px;
}
.woocommerce-onboarding-loader__paragraph {
font-size: 16px;
line-height: 24px;
color: #2f2f2f;
opacity: 0.8;
text-align: center;
@include breakpoint( '<782px' ) {
font-size: 14px;
line-height: 20px;
}
}
}

View File

@ -0,0 +1,132 @@
/**
* External dependencies
*/
import classNames from 'classnames';
import {
useState,
useEffect,
Children,
createElement,
Fragment,
} from '@wordpress/element';
import type { ReactNode } from 'react';
/**
* Internal dependencies
*/
import ProgressBar from './ProgressBar';
export const Loader = ( {
children,
className,
}: {
children: ReactNode;
className?: string;
} ) => {
return (
<div
className={ classNames(
'woocommerce-onboarding-loader',
className
) }
>
{ children }
</div>
);
};
type withClassName = {
className?: string;
};
type withReactChildren = {
children: ReactNode;
};
Loader.Layout = ( {
children,
className,
}: withClassName & withReactChildren ) => {
return (
<div
className={ classNames(
'woocommerce-onboarding-loader-wrapper',
className
) }
>
{ children }
</div>
);
};
Loader.Illustration = ( { children }: withReactChildren ) => {
return <>{ children }</>;
};
Loader.Title = ( {
children,
className,
}: withClassName & withReactChildren ) => {
return (
<h1
className={ classNames(
'woocommerce-onboarding-loader__title',
className
) }
>
{ children }
</h1>
);
};
Loader.ProgressBar = ( {
progress,
className,
}: { progress: number } & withClassName ) => {
return (
<ProgressBar
className={ classNames( 'progress-bar', className ) }
percent={ progress ?? 0 }
color={ 'var(--wp-admin-theme-color)' }
bgcolor={ '#E0E0E0' }
/>
);
};
Loader.Subtext = ( {
children,
className,
}: withReactChildren & withClassName ) => {
return (
<p
className={ classNames(
'woocommerce-onboarding-loader__paragraph',
className
) }
>
{ children }
</p>
);
};
const LoaderSequence = ( {
interval,
children,
}: { interval: number } & withReactChildren ) => {
const [ index, setIndex ] = useState( 0 );
useEffect( () => {
const rotateInterval = setInterval( () => {
setIndex(
( prevIndex ) => ( prevIndex + 1 ) % Children.count( children )
);
}, interval );
return () => clearInterval( rotateInterval );
}, [ interval, children ] );
const childToDisplay = Children.toArray( children )[ index ];
return <>{ childToDisplay }</>;
};
Loader.Sequence = LoaderSequence; // eslint rule-of-hooks can't handle the compound component definition directly

View File

@ -0,0 +1,48 @@
/**
* External dependencies
*/
import { createElement, HTMLAttributes } from 'react';
type ProgressBarProps = {
/** Component classname */
className?: string;
/** Progress percentage (0 to 100) */
percent?: number;
/** Color of the progress bar */
color?: string;
/** Background color of the progress container */
bgcolor?: string;
};
const ProgressBar = ( {
className = '',
percent = 0,
color = '#674399',
bgcolor = 'var(--wp-admin-theme-color)',
}: ProgressBarProps ) => {
const containerStyles = {
backgroundColor: bgcolor,
};
const fillerStyles: HTMLAttributes< HTMLDivElement >[ 'style' ] = {
backgroundColor: color,
width: `${ percent }%`,
display: percent === 0 ? 'none' : 'inherit',
};
return (
<div className={ `woocommerce-onboarding-progress-bar ${ className }` }>
<div
className="woocommerce-onboarding-progress-bar__container"
style={ containerStyles }
>
<div
className="woocommerce-onboarding-progress-bar__filler"
style={ fillerStyles }
/>
</div>
</div>
);
};
export default ProgressBar;

View File

@ -0,0 +1 @@
export { Loader } from './Loader';

View File

@ -0,0 +1,78 @@
/**
* External dependencies
*/
import React, { createElement } from 'react';
/**
* Internal dependencies
*/
import { Loader } from '../';
/** Simple straightforward example of how to use the <Loader> compound component */
export const ExampleSimpleLoader = () => (
<Loader>
<Loader.Layout>
<Loader.Illustration>
<img
src="https://placekitten.com/200/200"
alt="a cute kitteh"
/>
</Loader.Illustration>
<Loader.Title>Very Impressive Title</Loader.Title>
<Loader.ProgressBar progress={ 30 } />
<Loader.Sequence interval={ 1000 }>
<Loader.Subtext>Message 1</Loader.Subtext>
<Loader.Subtext>Message 2</Loader.Subtext>
<Loader.Subtext>Message 3</Loader.Subtext>
</Loader.Sequence>
</Loader.Layout>
</Loader>
);
/** <Loader> component story with controls */
const Template = ( { progress, title, messages } ) => (
<Loader>
<Loader.Layout>
<Loader.Illustration>
<img
src="https://placekitten.com/200/200"
alt="a cute kitteh"
/>
</Loader.Illustration>
<Loader.Title>{ title }</Loader.Title>
<Loader.ProgressBar progress={ progress } />
<Loader.Sequence interval={ 1000 }>
{ messages.map( ( message, index ) => (
<Loader.Subtext key={ index }>{ message }</Loader.Subtext>
) ) }
</Loader.Sequence>
</Loader.Layout>
</Loader>
);
export const ExampleLoaderWithControls = Template.bind( {} );
ExampleLoaderWithControls.args = {
title: 'Very Impressive Title',
progress: 30,
messages: [ 'Message 1', 'Message 2', 'Message 3' ],
};
export default {
title: 'WooCommerce Admin/Onboarding/Loader',
component: ExampleLoaderWithControls,
argTypes: {
title: {
control: 'text',
},
progress: {
control: {
type: 'range',
min: 0,
max: 100,
},
},
messages: {
control: 'object',
},
},
};

View File

@ -16,3 +16,4 @@ export { WooOnboardingTaskListItem } from './components/WooOnboardingTaskListIte
export { WooOnboardingTaskListHeader } from './components/WooOnboardingTaskListHeader';
export { WooOnboardingTask } from './components/WooOnboardingTask';
export * from './utils/countries';
export { Loader } from './components/Loader';

View File

@ -2,3 +2,4 @@
@import 'components/WCPayBanner/WCPayBanner.scss';
@import 'components/WCPayBenefits/WCPayBenefits.scss';
@import 'components/RecommendedRibbon/RecommendedRibbon.scss';
@import 'components/Loader/Loader.scss';

View File

@ -0,0 +1,62 @@
/**
* External dependencies
*/
import React from 'react';
import { Loader } from '@woocommerce/onboarding';
/**
* Internal dependencies
*/
import { CoreProfilerStateMachineContext } from '../..';
import { getLoaderStageMeta } from '../../utils/get-loader-stage-meta';
import './loader.scss';
export type Stage = {
title: string;
image?: string | JSX.Element;
paragraphs: Array< {
label: string;
text: string;
duration?: number;
element?: JSX.Element;
} >;
};
export type Stages = Array< Stage >;
export type LoaderContextProps = Pick<
CoreProfilerStateMachineContext,
'loader'
>;
export const CoreProfilerLoader = ( {
context,
}: {
context: LoaderContextProps;
} ) => {
const stages = getLoaderStageMeta( context.loader.useStages ?? 'default' );
const currentStage = stages[ context.loader.stageIndex ?? 0 ];
return (
<Loader className={ context.loader.className }>
<Loader.Layout>
<Loader.Illustration>
{ currentStage.image }
</Loader.Illustration>
<Loader.Title>{ currentStage.title }</Loader.Title>
<Loader.ProgressBar
progress={ context.loader?.progress ?? 0 }
/>
<Loader.Sequence interval={ 3000 }>
{ currentStage.paragraphs.map( ( paragraph, index ) => (
<Loader.Subtext key={ index }>
<b>{ paragraph?.label }</b>
{ paragraph?.text }
{ paragraph?.element }
</Loader.Subtext>
) ) }
</Loader.Sequence>
</Loader.Layout>
</Loader>
);
};

View File

@ -0,0 +1,8 @@
// Loader page
.woocommerce-onboarding-loader {
.loader-hearticon {
position: relative;
top: 2px;
left: 2px;
}
}

View File

@ -53,7 +53,7 @@ import {
} from './pages/BusinessInfo';
import { BusinessLocation } from './pages/BusinessLocation';
import { getCountryStateOptions } from './services/country';
import { Loader } from './pages/Loader';
import { CoreProfilerLoader } from './components/loader/Loader';
import { Plugins } from './pages/Plugins';
import { getPluginSlug, useFullScreen } from '~/utils';
import './style.scss';
@ -1123,7 +1123,7 @@ export const coreProfilerStateMachineDefinition = createMachine( {
},
},
meta: {
component: Loader,
component: CoreProfilerLoader,
},
},
},
@ -1188,7 +1188,7 @@ export const coreProfilerStateMachineDefinition = createMachine( {
],
},
meta: {
component: Loader,
component: CoreProfilerLoader,
},
},
plugins: {
@ -1250,7 +1250,7 @@ export const coreProfilerStateMachineDefinition = createMachine( {
],
},
meta: {
component: Loader,
component: CoreProfilerLoader,
progress: 100,
},
},
@ -1272,7 +1272,7 @@ export const coreProfilerStateMachineDefinition = createMachine( {
],
},
meta: {
component: Loader,
component: CoreProfilerLoader,
progress: 100,
},
},
@ -1303,7 +1303,7 @@ export const coreProfilerStateMachineDefinition = createMachine( {
},
},
meta: {
component: Loader,
component: CoreProfilerLoader,
progress: 100,
},
},
@ -1398,7 +1398,7 @@ export const coreProfilerStateMachineDefinition = createMachine( {
},
},
meta: {
component: Loader,
component: CoreProfilerLoader,
},
},
},

View File

@ -1,76 +0,0 @@
/**
* External dependencies
*/
import classNames from 'classnames';
import { useState, useEffect } from '@wordpress/element';
/**
* Internal dependencies
*/
import { CoreProfilerStateMachineContext } from '..';
import ProgressBar from '../components/progress-bar/progress-bar';
import { getLoaderStageMeta } from '../utils/get-loader-stage-meta';
export type Stage = {
title: string;
image?: string | JSX.Element;
paragraphs: Array< {
label: string;
text: string;
duration?: number;
element?: JSX.Element;
} >;
};
export type Stages = Array< Stage >;
export type LoaderContextProps = Pick<
CoreProfilerStateMachineContext,
'loader'
>;
export const Loader = ( { context }: { context: LoaderContextProps } ) => {
const stages = getLoaderStageMeta( context.loader.useStages ?? 'default' );
const currentStage = stages[ context.loader.stageIndex ?? 0 ];
const [ currentParagraph, setCurrentParagraph ] = useState( 0 );
useEffect( () => {
const interval = setInterval( () => {
setCurrentParagraph( ( _currentParagraph ) =>
currentStage.paragraphs[ _currentParagraph + 1 ]
? _currentParagraph + 1
: 0
);
}, currentStage.paragraphs[ currentParagraph ]?.duration ?? 3000 );
return () => clearInterval( interval );
}, [ currentParagraph, currentStage.paragraphs ] );
return (
<div
className={ classNames(
'woocommerce-profiler-loader',
context.loader.className
) }
>
<div className="woocommerce-profiler-loader-wrapper">
{ currentStage.image && currentStage.image }
<h1 className="woocommerce-profiler-loader__title">
{ currentStage.title }
</h1>
<ProgressBar
className={ 'progress-bar' }
percent={ context.loader.progress ?? 0 }
color={ 'var(--wp-admin-theme-color)' }
bgcolor={ '#E0E0E0' }
/>
<p className="woocommerce-profiler-loader__paragraph">
<b>
{ currentStage.paragraphs[ currentParagraph ]?.label }{ ' ' }
</b>
{ currentStage.paragraphs[ currentParagraph ]?.text }
{ currentStage.paragraphs[ currentParagraph ]?.element }
</p>
</div>
</div>
);
};

View File

@ -1,23 +1,25 @@
/**
* Internal dependencies
*/
import { Loader } from '../pages/Loader';
import { CoreProfilerLoader } from '../components/loader/Loader';
import { WithSetupWizardLayout } from './WithSetupWizardLayout';
import '../style.scss';
export const Short = () => (
<Loader
<CoreProfilerLoader
context={ { loader: { progress: 10, useStages: 'skipGuidedSetup' } } }
/>
);
export const Plugins = () => (
<Loader context={ { loader: { progress: 10, useStages: 'plugins' } } } />
<CoreProfilerLoader
context={ { loader: { progress: 10, useStages: 'plugins' } } }
/>
);
export default {
title: 'WooCommerce Admin/Application/Core Profiler/Loader',
component: Loader,
component: CoreProfilerLoader,
decorators: [ WithSetupWizardLayout ],
};

View File

@ -273,63 +273,6 @@
}
}
// Loader page
.woocommerce-profiler-loader {
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
@include breakpoint( '<782px' ) {
padding: 0 20px;
}
.loader-hearticon {
position: relative;
top: 2px;
left: 2px;
}
h1 {
padding: 0;
margin: 58px 0 0 0;
font-size: 28px;
font-weight: 500;
@include breakpoint( '<782px' ) {
font-size: 20px;
}
}
.woocommerce-profiler-loader-wrapper {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
max-width: 520px;
}
.woocommerce-profiler-progress-bar {
width: 520px;
margin: 16px 0 16px 0;
@include breakpoint( '<782px' ) {
width: 100%;
}
}
.woocommerce-profiler-progress-bar__container {
height: 4px;
}
.woocommerce-profiler-loader__paragraph {
font-size: 16px;
line-height: 24px;
color: #2f2f2f;
opacity: 0.8;
text-align: center;
@include breakpoint( '<782px' ) {
font-size: 14px;
line-height: 20px;
}
}
}
// User profile page
.woocommerce-profiler-user-profile {
.woocommerce-profiler-user-profile__content {

View File

@ -12,7 +12,7 @@ import LayoutImage from '../assets/images/loader-layout.svg';
import OpeningTheDoorsImage from '../assets/images/loader-openingthedoors.svg';
import Hearticon from '../assets/images/loader-hearticon.svg';
import { Stages } from '../pages/Loader';
import { Stages } from '../components/loader/Loader';
const LightbulbStage = {
title: __( 'Turning on the lights', 'woocommerce' ),

View File

@ -0,0 +1,4 @@
Significance: minor
Type: dev
Refactored core profiler loader to be more generalizable and moved to @woocommerce/onboarding

File diff suppressed because it is too large Load Diff

View File

@ -9,6 +9,8 @@ module.exports = {
'../../../packages/js/components/src/**/stories/*.@(js|tsx)',
// WooCommerce Admin / @woocommerce/experimental components
'../../../packages/js/experimental/src/**/stories/*.@(js|tsx)',
// WooCommerce Admin / @woocommerce/onboarding components
'../../../packages/js/onboarding/src/**/stories/*.@(js|tsx)',
'../../../plugins/woocommerce-admin/client/**/stories/*.@(js|tsx)',
],
addons: [
@ -44,11 +46,13 @@ module.exports = {
? `
<link href="experimental-css/style-rtl.css" rel="stylesheet" />
<link href="component-css/style-rtl.css" rel="stylesheet" />
<link href="onboarding-css/style-rtl.css" rel="stylesheet" />
<link href="app-css/style-rtl.css" rel="stylesheet" />
`
: `
<link href="component-css/style.css" rel="stylesheet" />
<link href="experimental-css/style.css" rel="stylesheet" />
<link href="onboarding-css/style.css" rel="stylesheet" />
<link href="app-css/style.css" rel="stylesheet" />
`
}

View File

@ -65,6 +65,13 @@ module.exports = ( storybookConfig ) => {
),
to: `./component-css/[name][ext]`,
},
{
from: path.resolve(
__dirname,
`../../packages/js/onboarding/build-style/*.css`
),
to: `./onboarding-css/[name][ext]`,
},
{
from: path.resolve(
__dirname,