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

Callouts in DCR - PR 1/5 - Add deadline component and separate new/legacy components into two folders #6741

Merged
merged 18 commits into from
Jan 3, 2023
Merged
Show file tree
Hide file tree
Changes from 17 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
21 changes: 16 additions & 5 deletions dotcom-rendering/src/web/components/Callout/CheckboxSelect.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,27 @@
import { Checkbox, CheckboxGroup } from '@guardian/source-react-components';
import { CampaignFieldCheckbox } from '../../../types/content';
import { FieldLabel } from './FieldLabel';
import type { CampaignFieldCheckbox } from 'src/types/content';

type Props = {
formField: CampaignFieldCheckbox;
formData: { [key in string]: string[] };
setFormData: React.Dispatch<React.SetStateAction<{ [x: string]: any }>>;
validationErrors?: { [key in string]: string };
};

export const CheckboxSelect = ({ formField, formData, setFormData }: Props) => (
export const CheckboxSelect = ({
formField,
formData,
setFormData,
validationErrors,
}: Props) => (
<>
<FieldLabel formField={formField} />
<CheckboxGroup name={formField.name}>
<CheckboxGroup
error={validationErrors?.[formField.id]}
hideLabel={formField.hideLabel}
name={formField.name}
label={formField.label}
supporting={formField.description}
>
{formField.options.map((option, index) => {
// data related to this field is mapped to `formData` using `formField.id`
// We cannot assume that the data exists, so we need to check if `formField.id` key exists in `formData`
Expand Down Expand Up @@ -39,6 +49,7 @@ export const CheckboxSelect = ({ formField, formData, setFormData }: Props) => (
label={option.label}
value={option.value}
checked={isCheckboxChecked}
error={validationErrors?.[formField.id] ? true : false}
onChange={() => {
setFormData({
...formData,
Expand Down
25 changes: 18 additions & 7 deletions dotcom-rendering/src/web/components/Callout/FieldLabel.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { css } from '@emotion/react';
import { neutral, textSans } from '@guardian/source-foundations';
import { CampaignFieldType } from '../../../types/content';
import {
neutral,
textSans,
visuallyHidden,
} from '@guardian/source-foundations';
import type { CampaignFieldType } from 'src/types/content';

const fieldLabelStyles = css`
${textSans.medium({ fontWeight: 'bold' })}
Expand All @@ -16,15 +20,22 @@ const optionalTextStyles = css`
padding-left: 5px;
`;

export const FieldLabel = ({ formField }: { formField: CampaignFieldType }) => (
<label css={fieldLabelStyles} htmlFor={formField.name}>
type Props = {
formField: CampaignFieldType;
hideLabel?: boolean;
};

export const FieldLabel = ({ formField, hideLabel }: Props) => (
<label
css={[fieldLabelStyles, hideLabel && visuallyHidden]}
htmlFor={formField.name}
hidden={formField.hideLabel}
>
{formField.label}
{!formField.required && <span css={optionalTextStyles}>Optional</span>}
{!!formField.description && (
<div>
<span css={fieldDescription}>
{`(${formField.description})`}
</span>
<span css={fieldDescription}>{`${formField.description}`}</span>
</div>
)}
</label>
Expand Down
127 changes: 101 additions & 26 deletions dotcom-rendering/src/web/components/Callout/FileUpload.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,79 @@
import { css } from '@emotion/react';
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the nice shiny new FileUpload component.

import { space, text, textSans } from '@guardian/source-foundations';
import { useState } from 'react';
import { CampaignFieldFile } from '../../../types/content';
import { stringifyFileBase64 } from '../../lib/stringifyFileBase64';
import type { SerializedStyles } from '@emotion/react';
import {
neutral,
palette,
remHeight,
space,
textSans,
visuallyHidden,
} from '@guardian/source-foundations';
import React, { useState } from 'react';
import type { CampaignFieldType } from 'src/types/content';
import { decidePalette } from 'src/web/lib/decidePalette';
import { stringifyFileBase64 } from 'src/web/lib/stringifyFileBase64';
import { FieldLabel } from './FieldLabel';

const fileUploadInputStyles = css`
padding-top: 10px;
padding-bottom: 10px;
`;

const errorMessagesStyles = css`
padding-top: ${space[2]}px;
color: ${text.error};
color: ${palette.error};
${textSans.small({ fontWeight: 'bold' })};
`;

const uploadStyles = css`
display: block;
`;

const textStyles = css`
${textSans.small()};
color: ${neutral[46]};
display: inline-block;
`;

type Props = {
formField: CampaignFieldFile;
format: ArticleFormat;
formField: CampaignFieldType;
formData: { [key in string]: any };
setFormData: React.Dispatch<React.SetStateAction<{ [x: string]: any }>>;
validationErrors: { [key in string]: string };
};

export const FileUpload = ({ formField, formData, setFormData }: Props) => {
const customUpload = (format: ArticleFormat): SerializedStyles => css`
${textSans.small()};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

presuming we don't need this as we have another text declaration below on line 59

color: ${decidePalette(format).text.richLink};
border: 1.5px solid ${decidePalette(format).text.richLink};
display: inline-flex;
justify-content: space-between;
align-items: center;
box-sizing: border-box;
background: transparent;
cursor: pointer;
transition: all 0.3s ease-in-out 0s;
text-decoration: none;
white-space: nowrap;
height: ${remHeight.ctaXsmall}rem;
min-height: ${remHeight.ctaXsmall}rem;
padding: ${space[3]}px;
margin: ${space[3]}px ${space[3]}px 0px 0px;
border-radius: ${remHeight.ctaMedium}rem;
${textSans.medium({ fontWeight: 'bold' })};
width: fit-content;
`;

export const FileUpload = ({
formField,
format,
formData,
setFormData,
}: Props) => {
const [chosenFile, setChosenFile] = useState<null | string>();
const [error, setError] = useState('');

const getFileName = (filepath?: string): string =>
filepath?.split(/(\\|\/)/g).pop() ?? '';

const onSelectFile = async (event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.files && event.target.files[0]) {
if (event.target.files?.[0]) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The && operator seemed unnecessary here, hence I removed it

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We even have a proposal to enforce this in the future: #6715

setError('');
try {
const stringifiedFile = await stringifyFileBase64(
Expand All @@ -35,27 +83,54 @@ export const FileUpload = ({ formField, formData, setFormData }: Props) => {
...formData,
[formField.id]: stringifiedFile,
});
setChosenFile(event.target.files[0].name);
} catch (e) {
setError(
'Sorry there was a problem with the file you uploaded above. Check the size and type. We only accept images, pdfs and .doc or .docx files',
'Sorry, there was a problem with the file you uploaded above. Check the size and type. We only accept images, pdfs and .doc or .docx files',
);
}
}
};

return (
<>
<FieldLabel formField={formField} />
<input
data-testid={`form-field-${formField.id}`}
css={fileUploadInputStyles}
type="file"
accept="image/*, .pdf"
required={formField.required}
onChange={onSelectFile}
/>
<p>We accept images and pdfs. Maximum total file size: 6MB</p>
{!!error && <div css={errorMessagesStyles}>{error}</div>}
<div css={uploadStyles}>
<FieldLabel formField={formField} />
<div>
<label css={customUpload(format)} htmlFor={formField.id}>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we use the FieldLabel here rather than having 2 labels for one component?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We still do use the FieldLabel on line 97, it's just that for accessibility/CSS reasons we had to wrap the 'Choose File' button in a <label> tag so that the whole form field (including the label) was not clickable. This also enables us to neatly mark what field this button is related to via the htmlFor attribute.

Copy link
Contributor

@sophie-macmillan sophie-macmillan Dec 20, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the field label a html label element? If we could wrap the Choose file button in the FieldLabel rather than the label the whole thing would be clickable and there wouldn't be two labels for one button which probably doesn't make sense semantically

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep I get what you mean here. The problem I found was that when the whole thing was clickable you had unexpected behaviour when the 'Remove File' button appeared after uploading a file - it would mean that when you click that button you're immediately prompted to upload a file again. Just a weird UX. However, if you think there is a way around this I'd be more than happy to pair on it!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think maybe this is all a moot conversation now as we've moved the file input into source. For reference in source the label wraps the Choose file div, and the Remove file button sits outside of the label

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @sophie-macmillan - I've removed this component now and have pushed the changes.

Choose file
<input
id={formField.id}
data-testid={`form-field-${formField.id}`}
type="file"
accept="image/*, .pdf"
required={formField.required}
onChange={onSelectFile}
css={css`
${visuallyHidden};
`}
/>
</label>
{chosenFile ? (
<>
<button
type="button"
css={customUpload(format)}
onClick={(): void => {
setChosenFile(undefined);
}}
>
Remove File
</button>
<span css={textStyles}>
{getFileName(chosenFile)}
</span>
</>
) : (
<div css={textStyles}> No file chosen </div>
)}
{!!error && <div css={errorMessagesStyles}>{error}</div>}
</div>
</div>
</>
);
};
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import type {
CampaignFieldCheckbox,
CampaignFieldRadio,
} from '../../../types/content';
} from 'src/types/content';
import { CheckboxSelect } from './CheckboxSelect';
import { RadioSelect } from './RadioSelect';

type Props = {
validationErrors?: { [key in string]: string };
formField: CampaignFieldCheckbox | CampaignFieldRadio;
formData: { [key in string]: any };
setFormData: React.Dispatch<React.SetStateAction<{ [x: string]: any }>>;
multiple: boolean;
};

export const MultiSelect = ({
validationErrors,
formField,
formData,
setFormData,
Expand All @@ -21,12 +23,14 @@ export const MultiSelect = ({
<div data-testid={`form-field-${formField.id}`}>
{multiple ? (
<CheckboxSelect
validationErrors={validationErrors ?? undefined}
formField={formField as CampaignFieldCheckbox}
formData={formData}
setFormData={setFormData}
/>
) : (
<RadioSelect
validationErrors={validationErrors ?? undefined}
formField={formField as CampaignFieldRadio}
formData={formData}
setFormData={setFormData}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import { css } from '@emotion/react';
import { Radio, RadioGroup } from '@guardian/source-react-components';
import type { CampaignFieldRadio } from '../../../types/content';
import type { CampaignFieldRadio } from 'src/types/content';
import { FieldLabel } from './FieldLabel';

type FieldProp = {
validationErrors?: { [key in string]: string };
formField: CampaignFieldRadio;
formData: { [key in string]: any };
setFormData: React.Dispatch<React.SetStateAction<{ [x: string]: any }>>;
};

export const RadioSelect = ({
validationErrors,
formField,
formData,
setFormData,
Expand All @@ -25,6 +27,7 @@ export const RadioSelect = ({
>
<FieldLabel formField={formField} />
<RadioGroup
error={validationErrors?.[formField.id]}
name={formField.name}
orientation={
formField.options.length > 2 ? 'vertical' : 'horizontal'
Expand Down
57 changes: 29 additions & 28 deletions dotcom-rendering/src/web/components/Callout/Select.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,37 @@
import { CampaignFieldSelect } from '../../../types/content';
import { FieldLabel } from './FieldLabel';
import { Select as SourceSelect } from '@guardian/source-react-components';
import type { CampaignFieldSelect } from 'src/types/content';

type Props = {
validationErrors?: { [key in string]: string };
formField: CampaignFieldSelect;
formData: { [key in string]: any };
setFormData: React.Dispatch<React.SetStateAction<{ [x: string]: any }>>;
};

export const Select = ({ formField, formData, setFormData }: Props) => (
<>
<FieldLabel formField={formField} />
<select
data-testid={`form-field-${formField.id}`}
required={formField.required}
value={
formField.id && formField.id in formData
? formData[formField.id]
: ''
}
onChange={(e) =>
setFormData({
...formData,
[formField.id]: e.target.value,
})
}
>
{formField.options &&
formField.options.map((option, index) => (
<option key={index} value={option.value}>
{option.value}
</option>
))}
</select>
</>
export const Select = ({
validationErrors,
formField,
formData,
setFormData,
}: Props) => (
<SourceSelect
hideLabel={formField.hideLabel}
data-testid={`form-field-${formField.id}`}
error={validationErrors?.[formField.id]}
label={formField.label}
supporting={formField.description}
value={formField.id in formData ? formData[formField.id] : ''}
onChange={(e) =>
setFormData({
...formData,
[formField.id]: e.target.value,
})
}
optional={!formField.required}
children={formField.options.map((option, index) => (
<option key={index} value={option.value}>
{option.value}
</option>
))}
/>
);
Loading