Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: changed

Connection UI: Make the disconnect survey a real radio group, drop the decorative arrow the options no longer need, and build the option cards on the shared Card component.
Comment thread
coder-karen marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { __ } from '@wordpress/i18n';
import { Text } from '@wordpress/ui';
import '../../disconnect-survey/_jp-connect_disconnect-survey-card.scss';
import DisconnectSurvey from '../../disconnect-survey';
import type { MouseEvent } from 'react';
Expand All @@ -7,7 +8,7 @@ interface StepSurveyProps {
/** Callback function used to close the modal and leave the disconnect flow. */
onExit?: ( e?: MouseEvent< HTMLElement > ) => void;
/** Callback function to handle submission of survey response. */
onFeedBackProvided?: ( answerId: string | undefined, answerText: string ) => void;
onFeedBackProvided?: ( answerId: string, answerText: string ) => void;
/** If the survey feedback is currently being saved/submitted. */
isSubmittingFeedback?: boolean;
}
Expand All @@ -22,9 +23,9 @@ const StepSurvey = ( { onExit, onFeedBackProvided, isSubmittingFeedback }: StepS
return (
<div className="jp-connection__disconnect-dialog__content">
<h1>{ __( 'Before you go, help us improve Jetpack', 'jetpack-connection-js' ) }</h1>
<p className="jp-connection__disconnect-dialog__large-text">
{ __( 'Let us know what didnt work for you', 'jetpack-connection-js' ) }
</p>
<Text className="jp-connection__disconnect-dialog__large-text">
{ __( "Let us know what didn't work for you", 'jetpack-connection-js' ) }
</Text>
<DisconnectSurvey
onSubmit={ onFeedBackProvided }
isSubmittingFeedback={ isSubmittingFeedback }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
&__large-text,
p.jp-connection__disconnect-dialog__large-text {
font-size: 1.25rem;
display: block;
margin-block: 0 1em;
}

&__link,
Expand Down Expand Up @@ -132,8 +134,14 @@
}

&__survey {
margin-bottom: 1.5rem;
display: flex;
flex-direction: column;
gap: 1.25rem;
margin: 1.25rem 0 1.5rem;
max-width: 100%;
border: 0;
padding: 0;
min-width: 0;
}

// The thank-you step wraps its copy in a @wordpress/ui Stack, so let the
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { jest } from '@jest/globals';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import StepSurvey from '../steps/step-survey';

describe( 'StepSurvey', () => {
const testProps = {
onExit: jest.fn(),
onFeedBackProvided: jest.fn(),
};

afterEach( () => {
testProps.onExit.mockClear();
testProps.onFeedBackProvided.mockClear();
} );

it( 'renders the heading and intro copy', () => {
render( <StepSurvey { ...testProps } /> );

expect( screen.getByRole( 'heading' ) ).toHaveTextContent(
'Before you go, help us improve Jetpack'
);
expect( screen.getByText( "Let us know what didn't work for you" ) ).toBeInTheDocument();
} );

it( 'renders the survey options', () => {
render( <StepSurvey { ...testProps } /> );
expect( screen.getAllByRole( 'radio' ) ).toHaveLength( 6 );
} );

it( 'passes the survey response up on submit', async () => {
const user = userEvent.setup();
render( <StepSurvey { ...testProps } /> );

await user.click( screen.getByText( "It's buggy." ) );
await user.click( screen.getByRole( 'button', { name: 'Submit Feedback' } ) );

expect( testProps.onFeedBackProvided ).toHaveBeenCalledWith( 'buggy', '' );
} );

it( 'calls onExit when "Skip for now" is clicked', async () => {
const user = userEvent.setup();
render( <StepSurvey { ...testProps } /> );

await user.click( screen.getByRole( 'link', { name: 'Skip for now' } ) );

expect( testProps.onExit ).toHaveBeenCalledTimes( 1 );
} );
} );
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
@use "@automattic/jetpack-base-styles/style";

// Layout, selected state and elevation on top of @wordpress/ui's Card, which
// supplies the surface itself.
.jp-connect__disconnect-survey-card {
--wp-ui-card-padding: 1rem;

border: 2px solid transparent;
width: 800px;
max-width: 100%;
margin-left: auto;
margin-right: auto;
border-radius: 4px;
text-align: left;
padding: 1rem;
position: relative;
box-shadow: 0 0 15px var(--jp-gray-off);

Expand All @@ -17,36 +19,36 @@
background: var(--jp-gray-off);
}

&::after {
content: "";
display: block;
width: 5px;
height: 5px;
position: absolute;
top: 50%;
right: 1.5rem;
border-top: 2px solid var(--jp-black);
border-right: 2px solid var(--jp-black);
transform: translateY(-50%) rotate(45deg);
}

&:hover {
cursor: pointer;
}

&:hover,
&:focus {
&:focus-within {

&:not(.jp-disconnect-survey-card--selected) {
&:not(.jp-connect__disconnect-survey-card--selected) {
border-color: var(--jp-black-80);
}
}

&__radio {
position: relative;
z-index: 1;
flex-shrink: 0;
margin: 0;
}

&__answer {
font-size: var(--font-body);
line-height: 1.5;
font-weight: 700;
margin: 0;
display: flex;
align-items: center;

&::before {
content: "";
position: absolute;
inset: 0;
}
}
}

Expand All @@ -55,9 +57,12 @@ input.jp-connect__disconnect-survey-card__input {
border: none;
background-color: transparent;
color: var(--jp-black-80);
cursor: text;
font-size: var(--font-body);
flex-grow: 1;
padding-right: 40px;
max-width: calc(100% - 40px);
min-width: 0;
position: relative;
z-index: 1;
}

.jp-connection__disconnect-dialog__content .jp-connection__disconnect-dialog__btn-back-to-wp {
Expand Down
123 changes: 57 additions & 66 deletions projects/js-packages/connection/components/disconnect-survey/index.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { __ } from '@wordpress/i18n';
import { Button } from '@wordpress/ui';
import { Fragment, useCallback, useState } from 'react';
import { Button, VisuallyHidden } from '@wordpress/ui';
import { Fragment, useCallback, useId, useState } from 'react';
import SurveyChoice from './survey-choice';
import type { ChangeEvent, KeyboardEvent } from 'react';
import type { ChangeEvent } from 'react';

interface DisconnectSurveyProps {
/** Callback handler function for when the survey response is submitted. */
onSubmit: ( answerId: string | undefined, answerText: string ) => void;
onSubmit?: ( answerId: string, answerText: string ) => void;
/** If the survey feedback is currently being saved/ submitted. */
isSubmittingFeedback?: boolean;
}
Expand All @@ -19,8 +19,12 @@ interface DisconnectSurveyProps {
*/
const DisconnectSurvey = ( props: DisconnectSurveyProps ) => {
const { onSubmit, isSubmittingFeedback } = props;
// The `name` shared by every radio in the survey, which is what groups them
// into one radio group. Generated per instance rather than hard-coded, so
// that two surveys on a page (though unlikely) stay separate groups with unique input IDs.
const surveyGroupName = `jp-connect__disconnect-survey${ useId() }`;
const [ selectedAnswer, setSelectedAnswer ] = useState< string >();
const [ customResponse, setCustomResponse ] = useState< string >();
const [ customResponse, setCustomResponse ] = useState( '' );

const options = [
{
Expand Down Expand Up @@ -57,56 +61,36 @@ const DisconnectSurvey = ( props: DisconnectSurveyProps ) => {
* Will send the survey response to the collection endpoint.
*/
const handleSurveySubmit = useCallback( () => {
// The submit button is disabled until an answer is picked.
if ( ! selectedAnswer ) {
return;
}

const answerText = selectedAnswer === customOption.id ? customResponse : '';
onSubmit( selectedAnswer, answerText );
onSubmit?.( selectedAnswer, answerText );
}, [ onSubmit, customOption.id, customResponse, selectedAnswer ] );

/**
* Handle input into the custom response field.
*
* @param {ChangeEvent<HTMLInputElement>} e - onChange event for the custom input
*/
const handleCustomResponse = useCallback(
( e: ChangeEvent< HTMLInputElement > ) => {
const value = e.target.value;
e.stopPropagation();
setCustomResponse( value );
},
[ setCustomResponse ]
);

/**
* Checks to see if an option is the currently selected option, returns a css class name if it matches.
* Select the "Other" option.
*
* @param {string} optionId - ID of the option to check for.
* @return {string} - The "selected" class if this option is currently selected.
* The free text field sits above the label's hit area. Interacting with the
* field is an unambiguous choice of "Other", so it selects the option itself.
*/
const selectedClass = ( optionId: string ) => {
if ( optionId === selectedAnswer ) {
return 'jp-connect__disconnect-survey-card--selected';
}

return '';
};
const selectCustomOption = useCallback( () => {
setSelectedAnswer( customOption.id );
}, [ customOption.id, setSelectedAnswer ] );

/**
* Event handler for keyboard events on the answer blocks.
* Handle input into the custom response field.
*
* @param {string} answerId - The slug of the answer that has been selected.
* @param {KeyboardEvent<HTMLDivElement>} e - Keydown event.
* @param {ChangeEvent<HTMLInputElement>} e - onChange event for the custom input
*/
const handleAnswerKeyDown = useCallback(
( answerId: string, e: KeyboardEvent< HTMLDivElement > ) => {
switch ( e.key ) {
case 'Enter':
case 'Space':
case 'Spacebar':
case ' ':
setSelectedAnswer( answerId );
break;
}
const handleCustomResponse = useCallback(
( e: ChangeEvent< HTMLInputElement > ) => {
setCustomResponse( e.target.value );
selectCustomOption();
},
[ setSelectedAnswer ]
[ selectCustomOption, setCustomResponse ]
);

/**
Expand All @@ -120,12 +104,11 @@ const DisconnectSurvey = ( props: DisconnectSurveyProps ) => {
<SurveyChoice
key={ option.id }
id={ option.id }
onClick={ setSelectedAnswer }
onKeyDown={ handleAnswerKeyDown }
className={ 'card jp-connect__disconnect-survey-card ' + selectedClass( option.id ) }
>
<p className="jp-connect__disconnect-survey-card__answer">{ option.answerText }</p>
</SurveyChoice>
name={ surveyGroupName }
label={ option.answerText }
checked={ selectedAnswer === option.id }
onSelect={ setSelectedAnswer }
/>
);
} );
};
Expand All @@ -141,31 +124,39 @@ const DisconnectSurvey = ( props: DisconnectSurveyProps ) => {
<SurveyChoice
id={ customOption.id }
key={ customOption.id }
onClick={ setSelectedAnswer }
onKeyDown={ handleAnswerKeyDown }
className={ 'card jp-connect__disconnect-survey-card ' + selectedClass( customOption.id ) }
name={ surveyGroupName }
label={ __( 'Other:', 'jetpack-connection-js' ) }
checked={ selectedAnswer === customOption.id }
onSelect={ setSelectedAnswer }
>
<p className="jp-connect__disconnect-survey-card__answer">
{ __( 'Other:', 'jetpack-connection-js' ) }{ ' ' }
<input
placeholder={ __( 'share your experience', 'jetpack-connection-js' ) }
className="jp-connect__disconnect-survey-card__input"
type="text"
value={ customResponse }
onChange={ handleCustomResponse }
maxLength={ 1000 } // Limit response length.
/>
</p>
<input
id="jp-connect__disconnect-survey-custom-input"
name="jp-connect__disconnect-survey-custom-input"
placeholder={ __( 'Share your experience', 'jetpack-connection-js' ) }
// Names the option as well as the field: the two are adjacent
// visually, but nothing otherwise ties this input to the
// "Other" radio for anyone reading it out of context.
aria-label={ __( 'Other: share your experience', 'jetpack-connection-js' ) }
className="jp-connect__disconnect-survey-card__input"
type="text"
value={ customResponse }
onChange={ handleCustomResponse }
onClick={ selectCustomOption }
maxLength={ 1000 } // Limit response length.
/>
</SurveyChoice>
);
};

return (
<Fragment>
<div className="jp-connection__disconnect-dialog__survey">
<fieldset className="jp-connection__disconnect-dialog__survey">
<VisuallyHidden render={ <legend /> }>
{ __( 'Why are you disconnecting?', 'jetpack-connection-js' ) }
</VisuallyHidden>
{ renderOptions() }
{ renderCustomOption() }
</div>
</fieldset>
<p>
<Button
disabled={ ! selectedAnswer || isSubmittingFeedback }
Expand Down
Loading
Loading