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:
parent
0e95435474
commit
0fc765beb3
|
@ -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={ [
|
||||
{
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
|
@ -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;
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
};
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: update
|
||||
|
||||
Add 'Tell me more' button to end of block editor tour for more information
|
Loading…
Reference in New Issue