Product Editor Onboarding: Add "tell me more" button to wc.com page (#38639)

* Copy Guide component from @wordpress/components and fix focus issue

* Add 'tell me more' button and behavior

* Move Guide component from components package to admin package
Implement assigning an href to the finish button, sending the current page and origin as a parameter on the onFinish callback

* Remove dependency

* Restore pnpm-lock.yaml

* Add changelog

* Add comment in Guide component

* Dismiss modal only when it's finished or X button is clicked

* Add 'rel' when opening link in a new tab
This commit is contained in:
Nathan Silveira 2023-06-12 14:54:29 -03:00 committed by GitHub
parent 0e95435474
commit 0fc765beb3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 424 additions and 12 deletions

View File

@ -2,23 +2,25 @@
* External dependencies
*/
import { Guide } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
/**
* Internal dependencies
*/
import Guide from '../components/guide';
import './style.scss';
interface Props {
onCloseGuide: () => void;
onCloseGuide: ( currentPage: number, origin: 'close' | 'finish' ) => void;
}
const BlockEditorGuide = ( { onCloseGuide }: Props ) => {
return (
<Guide
className="woocommerce-block-editor-guide"
finishButtonText={ __( 'Close', 'woocommerce' ) }
contentLabel=""
finishButtonText={ __( 'Tell me more', 'woocommerce' ) }
finishButtonLink="https://woocommerce.com/product-form-beta"
onFinish={ onCloseGuide }
pages={ [
{

View File

@ -34,14 +34,29 @@ const BlockEditorTour = ( { shouldTourBeShown, dismissModal }: Props ) => {
setIsGuideOpen( true );
};
const closeGuide = () => {
recordEvent( 'block_product_editor_spotlight_completed' );
setIsGuideOpen( false );
maybeShowFeedbackBar();
};
if ( isGuideOpen ) {
return <BlockEditorGuide onCloseGuide={ closeGuide } />;
return (
<BlockEditorGuide
onCloseGuide={ ( currentPage, source ) => {
dismissModal();
if ( source === 'finish' ) {
recordEvent(
'block_product_editor_spotlight_tell_me_more_click'
);
} else {
// adding 1 to consider the TourKit as page 0
recordEvent(
'block_product_editor_spotlight_dismissed',
{
current_page: currentPage + 1,
}
);
}
setIsGuideOpen( false );
maybeShowFeedbackBar();
} }
/>
);
} else if ( shouldTourBeShown ) {
return (
<TourKit
@ -82,15 +97,18 @@ const BlockEditorTour = ( { shouldTourBeShown, dismissModal }: Props ) => {
},
],
closeHandler: ( _steps, _currentStepIndex, source ) => {
dismissModal();
if ( source === 'done-btn' ) {
recordEvent(
'block_product_editor_spotlight_view_highlights'
);
openGuide();
} else {
dismissModal();
recordEvent(
'block_product_editor_spotlight_dismissed'
'block_product_editor_spotlight_dismissed',
{
current_page: 0,
}
);
maybeShowFeedbackBar();
}

View File

@ -0,0 +1,16 @@
/**
* External dependencies
*/
import { SVG, Circle } from '@wordpress/primitives';
import { createElement } from '@wordpress/element';
export const PageControlIcon = ( { isSelected }: { isSelected: boolean } ) => (
<SVG width="8" height="8" fill="none" xmlns="http://www.w3.org/2000/svg">
<Circle
cx="4"
cy="4"
r="4"
fill={ isSelected ? '#419ECD' : '#E1E3E6' }
/>
</SVG>
);

View File

@ -0,0 +1,123 @@
/**
* External dependencies
*/
import classnames from 'classnames';
import { useState, useRef, createElement } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { Modal, Button } from '@wordpress/components';
/**
* Internal dependencies
*/
import PageControl from './page-control';
import type { GuideProps } from './types';
/*
* This component was copied from @wordpress/components since we needed
* additional functionality and also found some issues.
* 1: The Close button was being focused every time the page changed.
* 2: It was not possible to know if the Guide was closed because the modal was closed or because the Finish button was clicked.
* 3: It was not possible to know which was the current page when the modal was closed.
* 4: It was not possible to provide a link to the Finish button.
*
* If/when all those are implemented at some point, we can migrate to the original component.
*/
function Guide( {
className,
contentLabel,
finishButtonText = __( 'Finish', 'woocommerce' ),
finishButtonLink,
onFinish,
pages = [],
}: GuideProps ) {
const guideContainer = useRef< HTMLDivElement >( null );
const [ currentPage, setCurrentPage ] = useState( 0 );
const canGoBack = currentPage > 0;
const canGoForward = currentPage < pages.length - 1;
const goBack = () => {
if ( canGoBack ) {
setCurrentPage( currentPage - 1 );
}
};
const goForward = () => {
if ( canGoForward ) {
setCurrentPage( currentPage + 1 );
}
};
if ( pages.length === 0 ) {
return null;
}
return (
<Modal
className={ classnames( 'components-guide', className ) }
title={ contentLabel }
onRequestClose={ () => {
onFinish( currentPage, 'close' );
} }
onKeyDown={ ( event ) => {
if ( event.code === 'ArrowLeft' ) {
goBack();
// Do not scroll the modal's contents.
event.preventDefault();
} else if ( event.code === 'ArrowRight' ) {
goForward();
// Do not scroll the modal's contents.
event.preventDefault();
}
} }
ref={ guideContainer }
>
<div className="components-guide__container">
<div className="components-guide__page">
{ pages[ currentPage ].image }
{ pages.length > 1 && (
<PageControl
currentPage={ currentPage }
numberOfPages={ pages.length }
setCurrentPage={ setCurrentPage }
/>
) }
{ pages[ currentPage ].content }
</div>
<div className="components-guide__footer">
{ canGoBack && (
<Button
className="components-guide__back-button"
onClick={ goBack }
>
{ __( 'Previous', 'woocommerce' ) }
</Button>
) }
{ canGoForward && (
<Button
className="components-guide__forward-button"
onClick={ goForward }
>
{ __( 'Next', 'woocommerce' ) }
</Button>
) }
{ ! canGoForward && (
<Button
className="components-guide__finish-button"
href={ finishButtonLink }
target={ finishButtonLink ? '_blank' : undefined }
rel={ finishButtonLink ? 'noopener' : undefined }
onClick={ () => onFinish( currentPage, 'finish' ) }
>
{ finishButtonText }
</Button>
) }
</div>
</div>
</Modal>
);
}
export default Guide;

View File

@ -0,0 +1,49 @@
/**
* External dependencies
*/
import { __, sprintf } from '@wordpress/i18n';
import { Button } from '@wordpress/components';
import { createElement } from '@wordpress/element';
/**
* Internal dependencies
*/
import { PageControlIcon } from './icons';
import type { PageControlProps } from './types';
export default function PageControl( {
currentPage,
numberOfPages,
setCurrentPage,
}: PageControlProps ) {
return (
<ul
className="components-guide__page-control"
aria-label={ __( 'Guide controls', 'woocommerce' ) }
>
{ Array.from( { length: numberOfPages } ).map( ( _, page ) => (
<li
key={ page }
// Set aria-current="step" on the active page, see https://www.w3.org/TR/wai-aria-1.1/#aria-current
aria-current={ page === currentPage ? 'step' : undefined }
>
<Button
key={ page }
icon={
<PageControlIcon
isSelected={ page === currentPage }
/>
}
aria-label={ sprintf(
/* translators: 1: current page number 2: total number of pages */
__( 'Page %1$d of %2$d', 'woocommerce' ),
page + 1,
numberOfPages
) }
onClick={ () => setCurrentPage( page ) }
/>
</li>
) ) }
</ul>
);
}

View File

@ -0,0 +1,132 @@
.components-guide {
$image-height: 300px;
$image-width: 320px;
@include break-small() {
width: 600px;
}
.components-modal__content {
padding: 0;
margin-top: 0;
border-radius: $radius-block-ui;
&::before {
content: none;
}
}
.components-modal__header {
border-bottom: none;
padding: 0;
position: sticky;
height: $header-height;
.components-button {
align-self: flex-start;
margin: $grid-unit-10 $grid-unit-10 0 0;
position: static;
&:hover {
svg {
fill: #fff;
}
}
}
}
&__container {
display: flex;
flex-direction: column;
justify-content: space-between;
margin-top: -$header-height;
min-height: 100%;
}
&__page {
display: flex;
flex-direction: column;
justify-content: center;
position: relative;
@include break-small() {
min-height: $image-height;
}
}
&__footer {
align-content: center;
display: flex;
height: 30px;
justify-content: center;
margin: 0 0 $grid-unit-30 0;
padding: 0 $grid-unit-40;
position: relative;
width: 100%;
}
&__page-control {
margin: 0;
text-align: center;
li {
display: inline-block;
margin: 0;
}
.components-button {
height: 30px;
min-width: 20px;
margin: -6px 0;
}
}
}
.components-modal__frame.components-guide {
border: none;
min-width: 312px;
height: 80vh;
max-height: 575px;
@media ( max-width: $break-small ) {
margin: auto;
max-width: calc(100vw - #{$grid-unit-20} * 2);
}
}
.components-button {
&.components-guide__back-button,
&.components-guide__forward-button,
&.components-guide__finish-button {
height: 30px;
position: absolute;
}
&.components-guide__back-button,
&.components-guide__forward-button {
font-size: $default-font-size;
padding: 4px 2px;
&.has-text svg {
margin: 0;
}
&:hover {
text-decoration: underline;
}
}
&.components-guide__back-button {
left: $grid-unit-40;
}
&.components-guide__forward-button {
right: $grid-unit-40;
color: #1386bf;
font-weight: bold;
}
&.components-guide__finish-button {
right: $grid-unit-40;
}
}

View File

@ -0,0 +1,68 @@
/**
* External dependencies
*/
import type { ReactNode } from 'react';
export type Page = {
/**
* Content of the page.
*/
content: ReactNode;
/**
* Image displayed above the page content.
*/
image?: ReactNode;
};
export type GuideProps = {
/**
* Deprecated. Use `pages` prop instead.
*
* @deprecated since 5.5
*/
children?: ReactNode;
/**
* A custom class to add to the modal.
*/
className?: string;
/**
* Used as the modal's accessibility label.
*/
contentLabel: string;
/**
* Use this to customize the label of the _Finish_ button shown at the end of the guide.
*
* @default 'Finish'
*/
finishButtonText?: string;
/**
* Use this to customize href of the _Finish_ button shown at the end of the guide.
*
*/
finishButtonLink?: string;
/**
* A function which is called when the guide is closed, either through closing the dialog or clicking the Finish button.
*/
onFinish: ( currentPage: number, origin: 'close' | 'finish' ) => void;
/**
* A list of objects describing each page in the guide. Each object **must** contain a `'content'` property and may optionally contain a `'image'` property.
*
* @default []
*/
pages?: Page[];
};
export type PageControlProps = {
/**
* Current page index.
*/
currentPage: number;
/**
* Total number of pages.
*/
numberOfPages: number;
/**
* Called when user clicks on a `PageControlIcon` button.
*/
setCurrentPage: ( page: number ) => void;
};

View File

@ -0,0 +1,4 @@
Significance: minor
Type: update
Add 'Tell me more' button to end of block editor tour for more information