Skip to content

Commit

Permalink
DEV-5640: Migrate DReason to TypeScript
Browse files Browse the repository at this point in the history
  • Loading branch information
nrh-cklimas committed Jan 15, 2025
1 parent 3754131 commit 2dbab8e
Show file tree
Hide file tree
Showing 11 changed files with 419 additions and 20 deletions.
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

0 comments on commit 2dbab8e

Please sign in to comment.