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:
parent
db2343cfed
commit
f13564419b
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Add FeedbackModal and ProductMVPFeedbackModal components
|
|
@ -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;
|
||||
}
|
|
@ -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 };
|
|
@ -0,0 +1 @@
|
|||
export * from './feedback-modal';
|
|
@ -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();
|
||||
} );
|
||||
} );
|
|
@ -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';
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export * from './product-mvp-feedback-modal';
|
|
@ -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%;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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={ __(
|
||||
'We’re 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 };
|
|
@ -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' ],
|
||||
''
|
||||
);
|
||||
} );
|
||||
} );
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue