Add feedback modal and product mvp feedback modal components (#36532)

* Add FeedbackModal component

* Add ProductMVPFeedbackModal package

* Add ref

* Fix `Send feedback` button type

* Add changelog

* Rename a few props

Co-authored-by: Fernando Marichal <contacto@fernandomarichal.com>
This commit is contained in:
Fernando Marichal 2023-01-23 20:19:37 -03:00 committed by GitHub
parent db2343cfed
commit f13564419b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 415 additions and 0 deletions

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add FeedbackModal and ProductMVPFeedbackModal components

View File

@ -0,0 +1,12 @@
.woocommerce-feedback-modal__buttons {
text-align: right;
.components-button {
margin-left: 1em;
}
}
.woocommerce-feedback-modal .woocommerce-feedback-modal__description {
max-width: 550px;
margin: 0 0 1.5em 0;
}

View File

@ -0,0 +1,106 @@
/**
* External dependencies
*/
import { createElement, useState } from '@wordpress/element';
import PropTypes from 'prop-types';
import { Button, Modal } from '@wordpress/components';
import { Text } from '@woocommerce/experimental';
import { __ } from '@wordpress/i18n';
/**
* Provides a modal requesting customer feedback.
*
* Answers and comments are sent to a callback function.
*
* @param {Object} props Component props.
* @param {Function} props.onSubmit Function to call when the results are sent.
* @param {string} props.title Title displayed in the modal.
* @param {string} props.description Description displayed in the modal.
* @param {string} props.isSubmitButtonDisabled Boolean to enable/disable the send button.
* @param {string} props.submitButtonLabel Label for the send button.
* @param {string} props.cancelButtonLabel Label for the cancel button.
* @param {Function} props.onModalClose Callback for when user closes modal by clicking cancel.
* @param {Function} props.children Children to be rendered.
*/
function FeedbackModal( {
onSubmit,
title,
description,
onModalClose,
children,
isSubmitButtonDisabled,
submitButtonLabel,
cancelButtonLabel,
}: {
onSubmit: () => void;
title: string;
description?: string;
onModalClose?: () => void;
children?: JSX.Element;
isSubmitButtonDisabled?: boolean;
submitButtonLabel?: string;
cancelButtonLabel?: string;
} ): JSX.Element | null {
const [ isOpen, setOpen ] = useState( true );
const closeModal = () => {
setOpen( false );
if ( onModalClose ) {
onModalClose();
}
};
if ( ! isOpen ) {
return null;
}
return (
<Modal
className="woocommerce-feedback-modal"
title={ title }
onRequestClose={ closeModal }
shouldCloseOnClickOutside={ false }
>
<Text
variant="body"
as="p"
className="woocommerce-feedback-modal__description"
size={ 14 }
lineHeight="20px"
marginBottom="1.5em"
>
{ description }
</Text>
{ children }
<div className="woocommerce-feedback-modal__buttons">
<Button isTertiary onClick={ closeModal } name="cancel">
{ cancelButtonLabel }
</Button>
<Button
isPrimary={ ! isSubmitButtonDisabled }
isSecondary={ isSubmitButtonDisabled }
onClick={ () => {
onSubmit();
setOpen( false );
} }
name="send"
disabled={ isSubmitButtonDisabled }
>
{ submitButtonLabel }
</Button>
</div>
</Modal>
);
}
FeedbackModal.propTypes = {
onSubmit: PropTypes.func.isRequired,
title: PropTypes.string,
description: PropTypes.string,
onModalClose: PropTypes.func,
isSubmitButtonDisabled: PropTypes.bool,
submitButtonLabel: PropTypes.string,
cancelButtonLabel: PropTypes.string,
};
export { FeedbackModal };

View File

@ -0,0 +1 @@
export * from './feedback-modal';

View File

@ -0,0 +1,54 @@
/**
* External dependencies
*/
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
import { createElement } from '@wordpress/element';
/**
* Internal dependencies
*/
import { FeedbackModal } from '../index';
const mockRecordScoreCallback = jest.fn();
describe( 'FeedbackModal', () => {
it( 'should render a modal', async () => {
render(
<FeedbackModal
onSubmit={ mockRecordScoreCallback }
title="Testing"
submitButtonLabel="Send"
cancelButtonLabel="Cancel"
/>
);
// Wait for the modal to render.
await screen.findByRole( 'dialog' );
expect(
screen.getByRole( 'button', { name: /Send/i } )
).toBeInTheDocument();
expect(
screen.getByRole( 'button', { name: /Cancel/i } )
).toBeInTheDocument();
} );
it( 'should close modal when cancel button pressed', async () => {
render(
<FeedbackModal
onSubmit={ mockRecordScoreCallback }
title="Testing"
submitButtonLabel="Send"
cancelButtonLabel="Cancel"
/>
);
// Wait for the modal to render.
await screen.findByRole( 'dialog' );
// Press cancel button.
fireEvent.click( screen.getByRole( 'button', { name: /Cancel/i } ) );
expect( screen.queryByRole( 'dialog' ) ).not.toBeInTheDocument();
} );
} );

View File

@ -1,3 +1,5 @@
export * from './customer-effort-score';
export * from './customer-feedback-simple';
export * from './customer-feedback-modal';
export * from './product-mvp-feedback-modal';
export * from './feedback-modal';

View File

@ -0,0 +1 @@
export * from './product-mvp-feedback-modal';

View File

@ -0,0 +1,23 @@
.woocommerce-product-mvp-feedback-modal {
&__subtitle {
margin-top: $gap-smaller !important;
}
&__checkboxes {
margin: $gap-small 0;
}
&__comments {
margin-top: 2em;
margin-bottom: 1.5em;
label {
display: block;
font-weight: bold;
text-transform: none;
font-size: 14px;
}
textarea {
width: 100%;
}
}
}

View File

@ -0,0 +1,156 @@
/**
* External dependencies
*/
import { createElement, Fragment, useState } from '@wordpress/element';
import PropTypes from 'prop-types';
import { CheckboxControl, TextareaControl } from '@wordpress/components';
import { Text } from '@woocommerce/experimental';
import { __ } from '@wordpress/i18n';
/**
* Internal dependencies
*/
import { FeedbackModal } from '../feedback-modal';
/**
* Provides a modal requesting customer feedback.
*
*
* @param {Object} props Component props.
* @param {Function} props.recordScoreCallback Function to call when the results are sent.
* @param {Function} props.onCloseModal Callback for when user closes modal by clicking cancel.
*/
function ProductMVPFeedbackModal( {
recordScoreCallback,
onCloseModal,
}: {
recordScoreCallback: ( checked: string[], comments: string ) => void;
onCloseModal?: () => void;
} ): JSX.Element | null {
const [ missingFeatures, setMissingFeatures ] = useState( false );
const [ missingPlugins, setMissingPlugins ] = useState( false );
const [ difficultToUse, setDifficultToUse ] = useState( false );
const [ slowBuggyOrBroken, setSlowBuggyOrBroken ] = useState( false );
const [ other, setOther ] = useState( false );
const checkboxes = [
{
key: 'missing-features',
label: __( 'Missing features', 'woocommerce' ),
checked: missingFeatures,
onChange: setMissingFeatures,
},
{
key: 'missing-plugins',
label: __( 'Missing plugins', 'woocommerce' ),
checked: missingPlugins,
onChange: setMissingPlugins,
},
{
key: 'difficult-to-use',
label: __( 'It is difficult to use', 'woocommerce' ),
checked: difficultToUse,
onChange: setDifficultToUse,
},
{
key: 'slow-buggy-or-broken',
label: __( 'It is slow, buggy, or broken', 'woocommerce' ),
checked: slowBuggyOrBroken,
onChange: setSlowBuggyOrBroken,
},
{
key: 'other',
label: __( 'Other (describe below)', 'woocommerce' ),
checked: other,
onChange: setOther,
},
];
const [ comments, setComments ] = useState( '' );
const onSendFeedback = () => {
const checked = checkboxes
.filter( ( checkbox ) => checkbox.checked )
.map( ( checkbox ) => checkbox.key );
recordScoreCallback( checked, comments );
};
const isSendButtonDisabled =
! comments &&
! missingFeatures &&
! missingPlugins &&
! difficultToUse &&
! slowBuggyOrBroken &&
! other;
return (
<FeedbackModal
title={ __(
'Thanks for trying out the new product editor!',
'woocommerce'
) }
description={ __(
'Were working on making it better, and your feedback will help improve the experience for thousands of merchants like you.',
'woocommerce'
) }
onSubmit={ onSendFeedback }
onModalClose={ onCloseModal }
isSubmitButtonDisabled={ isSendButtonDisabled }
submitButtonLabel={ __( 'Send feedback', 'woocommerce' ) }
cancelButtonLabel={ __( 'Skip', 'woocommerce' ) }
>
<>
<Text
variant="subtitle.small"
as="p"
weight="600"
size="14"
lineHeight="20px"
>
{ __(
'What made you switch back to the classic product editor?',
'woocommerce'
) }
</Text>
<Text
weight="400"
size="12"
as="p"
lineHeight="16px"
color="#757575"
className="woocommerce-product-mvp-feedback-modal__subtitle"
>
{ __( '(Check all that apply)', 'woocommerce' ) }
</Text>
<div className="woocommerce-product-mvp-feedback-modal__checkboxes">
{ checkboxes.map( ( checkbox, index ) => (
<CheckboxControl
key={ index }
label={ checkbox.label }
name={ checkbox.key }
checked={ checkbox.checked }
onChange={ checkbox.onChange }
/>
) ) }
</div>
<div className="woocommerce-product-mvp-feedback-modal__comments">
<TextareaControl
label={ __( 'Additional comments', 'woocommerce' ) }
value={ comments }
placeholder={ __(
'Optional, but much apprecated. We love reading your feedback!',
'woocommerce'
) }
onChange={ ( value: string ) => setComments( value ) }
rows={ 5 }
/>
</div>
</>
</FeedbackModal>
);
}
ProductMVPFeedbackModal.propTypes = {
recordScoreCallback: PropTypes.func.isRequired,
onCloseModal: PropTypes.func,
};
export { ProductMVPFeedbackModal };

View File

@ -0,0 +1,54 @@
/**
* External dependencies
*/
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
import { createElement } from '@wordpress/element';
/**
* Internal dependencies
*/
import { ProductMVPFeedbackModal } from '../index';
const mockRecordScoreCallback = jest.fn();
describe( 'ProductMVPFeedbackModal', () => {
it( 'should close the ProductMVPFeedback modal when skip button pressed', async () => {
render(
<ProductMVPFeedbackModal
recordScoreCallback={ mockRecordScoreCallback }
/>
);
// Wait for the modal to render.
await screen.findByRole( 'dialog' );
// Press cancel button.
fireEvent.click( screen.getByRole( 'button', { name: /Skip/i } ) );
expect( screen.queryByRole( 'dialog' ) ).not.toBeInTheDocument();
} );
it( 'should enable Send button when an option is checked', async () => {
render(
<ProductMVPFeedbackModal
recordScoreCallback={ mockRecordScoreCallback }
/>
);
// Wait for the modal to render.
await screen.findByRole( 'dialog' );
fireEvent.click( screen.getByRole( 'checkbox', { name: /other/i } ) );
fireEvent.click(
screen.getByRole( 'button', { name: /Send feedback/i } )
);
} );
it( 'should call the function sent as recordScoreCallback with the checked options', async () => {
render(
<ProductMVPFeedbackModal
recordScoreCallback={ mockRecordScoreCallback }
/>
);
// Wait for the modal to render.
await screen.findByRole( 'dialog' );
fireEvent.click( screen.getByRole( 'checkbox', { name: /other/i } ) );
expect( mockRecordScoreCallback ).toHaveBeenCalledWith(
[ 'other' ],
''
);
} );
} );

View File

@ -1,4 +1,6 @@
@import 'customer-feedback-simple/customer-feedback-simple.scss';
@import 'product-mvp-feedback-modal/product-mvp-feedback-modal.scss';
@import 'feedback-modal/feedback-modal.scss';
.woocommerce-customer-effort-score__selection {
margin: 1em 0 1.5em 0;