Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DEV-5640: Migrate DReason to TypeScript #1694

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
19 changes: 9 additions & 10 deletions spa/cypress/e2e/01-donationPage.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -189,15 +189,14 @@ describe('Reason for Giving element', () => {
cy.getByTestId('d-reason').should('exist');
});

it('should render picklist with options', () => {
cy.getByTestId('excited-to-support-picklist').should('exist');
cy.getByTestId('excited-to-support-picklist').click();
it('should render select with options', () => {
cy.getByTestId('reason-for-giving-reason-select').should('exist');
cy.getByTestId('reason-for-giving-reason-select').click();
cy.findByRole('option', { name: 'test1' }).click();
});

it('should not show "honoree/in_memory_of" input if "No" is selected', () => {
// tribute_type "No" has value "", hense `tribute-""`.
cy.getByTestId('tribute-')
cy.getByTestId('tribute-type-null')
.get('input')
.then(($input) => {
cy.log($input);
Expand All @@ -208,10 +207,10 @@ describe('Reason for Giving element', () => {
});

it('should show tribute input if honoree or in_memory_of is selected', () => {
cy.getByTestId('tribute-type_honoree').click();
cy.getByTestId('tribute-type-honoree').click();
cy.getByTestId('tribute-input').should('exist');

cy.getByTestId('tribute-type_in_memory_of').click();
cy.getByTestId('tribute-type-inMemoryOf').click();
cy.getByTestId('tribute-input').should('exist');
});

Expand Down Expand Up @@ -247,13 +246,13 @@ describe('Reason for Giving element', () => {
cy.wait('@getPageDetail');

// Assert that we don't go to the payment modal if we don't pick a reason.
// We should be blocked by browser validation.

fillOutDonorInfoSection();
fillOutAddressSection();
cy.get('form')
.findByRole('button', { name: /Continue to Payment/ })
.click();
cy.get('#dreason-select-helper-text').contains(validationError);
cy.get('form #stripe-payment-element').should('not.exist');
});

Expand Down Expand Up @@ -454,9 +453,9 @@ function fillOutDonorInfoSection() {
}

function fillOutReasonForGiving() {
cy.get('[data-testid="excited-to-support-picklist"]').click();
cy.get('[data-testid="reason-for-giving-reason-select"]').click();
cy.findByRole('option', { name: 'test1' }).click();
cy.get('[data-testid="excited-to-support-picklist"]').invoke('val').as('reasonValue');
cy.get('[data-testid="reason-for-giving-reason-select"]').invoke('val').as('reasonValue');
}

const fakeEmailHash = 'b4170aca0fd3e60';
Expand Down
60 changes: 60 additions & 0 deletions spa/src/components/donationPage/pageContent/DReason/DReason.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import PropTypes, { InferProps } from 'prop-types';
import { useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { usePage } from 'components/donationPage/DonationPage';
import { ReasonElement } from 'hooks/useContributionPage';
import DElement from '../DElement';
import TributeFields, { TributeFieldsProps } from './TributeFields';
import ReasonFields from './ReasonFields';

const DReasonPropTypes = {
element: PropTypes.object.isRequired
};

export interface DReasonProps extends InferProps<typeof DReasonPropTypes> {
element: ReasonElement;
}

export function DReason({ element }: DReasonProps) {
const { errors } = usePage();
const [selectedReason, setSelectedReason] = useState<string>('');
const [reasonText, setReasonText] = useState<string>('');
const [tributeType, setTributeType] = useState<TributeFieldsProps['tributeType']>(null);
const [tributeName, setTributeName] = useState('');
const { t } = useTranslation();
const required = useMemo(() => element.requiredFields.includes('reason_for_giving'), [element.requiredFields]);

return (
<DElement label={t('donationPage.dReason.reasonForGiving')} data-testid="d-reason">
<ReasonFields
onChangeOption={setSelectedReason}
onChangeText={setReasonText}
optionError={errors.reason_for_giving}
options={element.content.reasons}
required={required}
selectedOption={selectedReason}
text={reasonText}
textError={errors.reason_other}
/>
{(element.content.askHonoree || element.content.askInMemoryOf) && (
<TributeFields
askHonoree={element.content.askHonoree}
askInMemoryOf={element.content.askInMemoryOf}
error={errors.reason_for_giving}
onChangeTributeName={setTributeName}
onChangeTributeType={setTributeType}
tributeName={tributeName}
tributeType={tributeType}
/>
)}
</DElement>
);
}

DReason.propTypes = DReasonPropTypes;
DReason.type = 'DReason';
DReason.displayName = 'Reason for Giving';
DReason.description = "Collect information about the contributor's reason for giving";
DReason.required = false;
DReason.unique = true;
export default DReason;
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import styled from 'styled-components';
import { Select as BaseSelect, TextField as BaseTextField } from 'components/base';

export const Root = styled.div`
display: grid;
gap: 25px;
`;

export const Select = styled(BaseSelect)`
&& {
.NreTextFieldInputLabelRoot {
font-weight: 500;
margin-bottom: 0.5em;
}

.NreTextFieldInputLabelAsterisk {
color: #ff476c;
}

.NreSelectMenu {
background: ${({ theme }) => theme.colors.cstm_inputBackground};
border: 1px solid #080708;
border-color: ${({ theme }) => theme.colors.cstm_inputBorder};
border-radius: 3px;
padding: 1rem;

&:focus {
border-color: ${({ theme }) => theme.colors.cstm_CTAs};
}
}
}
`;

export const TextField = styled(BaseTextField)`
&& {
.NreTextFieldInput {
background: ${({ theme }) => theme.colors.cstm_inputBackground};
border-color: ${({ theme }) => theme.colors.cstm_inputBorder};
border-width: 1px;
}

.NreTextFieldInputLabelAsterisk {
color: #ff476c;
}
}
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { Collapse } from '@material-ui/core';
import PropTypes, { InferProps } from 'prop-types';
import { useTranslation } from 'react-i18next';
import { useMemo } from 'react';
import { Root, Select, TextField } from './ReasonFields.styled';

// We need to track both a selected reason and a user-entered reason so that if
// the user types in a reason that exactly matches a pre-selected one, the text
// field doesn't disappear. In other words, if we used a single value, then we
// wouldn't be able to distinguish between a user who chose a pre-selected value
// and one who typed one in that matched a pre-selected value, because we
// conditionally show the text field in this component depending on the state of
// the select.

const ReasonFieldsPropTypes = {
onChangeText: PropTypes.func.isRequired,
onChangeOption: PropTypes.func.isRequired,
optionError: PropTypes.string,
options: PropTypes.arrayOf(PropTypes.string.isRequired).isRequired,
required: PropTypes.bool,
selectedOption: PropTypes.string.isRequired,
text: PropTypes.string.isRequired,
textError: PropTypes.string
};

export interface ReasonFieldsProps extends InferProps<typeof ReasonFieldsPropTypes> {
onChangeText: (value: string) => void;
onChangeOption: (value: string) => void;
}

export function ReasonFields({
onChangeOption,
onChangeText,
optionError,
options,
required,
selectedOption,
text,
textError
}: ReasonFieldsProps) {
const { t } = useTranslation();
const otherReasonLabel = t('donationPage.dReason.other');
const optionsWithOther = useMemo(() => {
if (options.length > 0) {
return [
...options.map((option) => ({ label: option, value: option })),
{ label: otherReasonLabel, value: otherReasonLabel }
];
}

return [];
}, [options, otherReasonLabel]);

// Names on inputs below must be set exactly in order for the form to be
// submitted properly.
//
// We also need to ensure the text field unmounts when transitioned out so
// it's not present in the DOM at form submission time, so its value doesn't
// appear in the form data.

return (
<Root>
{optionsWithOther.length > 0 && (
<Select
data-testid="reason-for-giving-reason-select"
error={!!optionError}
fullWidth
helperText={optionError}
label={t('donationPage.dReason.supportWork')}
name="reason_for_giving"
onChange={(e) => onChangeOption(e.target.value)}
options={optionsWithOther}
required={!!required}
value={selectedOption}
></Select>
)}
<Collapse in={optionsWithOther.length === 0 || selectedOption === otherReasonLabel} unmountOnExit>
<TextField
error={!!textError}
fullWidth
helperText={textError}
label={t('donationPage.dReason.tellUsWhy')}
name="reason_other"
onChange={(event) => onChangeText(event.target.value)}
required={!!required}
value={text}
/>
</Collapse>
</Root>
);
}

ReasonFields.propTypes = ReasonFieldsPropTypes;
export default ReasonFields;
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import styled from 'styled-components';
// Using the MUI core radio controls to match appearance in DFrequency.
import { FormControlLabel as MuiFormControlLabel, Radio as MuiRadio } from '@material-ui/core';
import { Checkbox as BaseCheckbox, TextField as BaseTextField } from 'components/base';

export const Prompt = styled.p`
font-size: ${({ theme }) => theme.fontSizesUpdated.md};
font-weight: 500;
margin-bottom: 6px;
padding-top: 25px;
`;

export const Checkbox = styled(BaseCheckbox)`
&& {
&.Mui-checked {
color: ${({ theme }) => theme.colors.cstm_CTAs};
}
}
`;

export const Radio = styled(MuiRadio)``;

export const RadioFormControlLabel = styled(MuiFormControlLabel)`
&& span {
font-size: ${({ theme }) => theme.fontSizesUpdated.md};
}
`;

export const Root = styled.div`
display: grid;
gap: 25px;
`;

export const TextField = styled(BaseTextField)`
&& {
.NreTextFieldInput {
background: ${({ theme }) => theme.colors.cstm_inputBackground};
border-color: ${({ theme }) => theme.colors.cstm_inputBorder};
border-width: 1px;
}

.NreTextFieldInputLabelAsterisk {
color: #ff476c;
}
}
`;
Loading
Loading