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-effort-score';
|
||||||
export * from './customer-feedback-simple';
|
export * from './customer-feedback-simple';
|
||||||
export * from './customer-feedback-modal';
|
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 '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 {
|
.woocommerce-customer-effort-score__selection {
|
||||||
margin: 1em 0 1.5em 0;
|
margin: 1em 0 1.5em 0;
|
||||||
|
|
Loading…
Reference in New Issue