Skip to content

Commit

Permalink
UX
Browse files Browse the repository at this point in the history
  • Loading branch information
emilysaffron committed Jun 17, 2024
1 parent d30f30a commit 37e078c
Show file tree
Hide file tree
Showing 17 changed files with 298 additions and 177 deletions.
1 change: 1 addition & 0 deletions .storybook/preview-head.html
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

<link
rel="preload"
href="fonts/ReithQalam/normal.woff2"
Expand Down
Binary file not shown.
2 changes: 2 additions & 0 deletions src/app/lib/config/services/mundo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,8 @@ export const service: DefaultServiceConfig = {
validationFilesInvalidType:
"Sorry, we can't use this type of file. Please add {{fileTypes}}.",
validationFilesTooSmall: 'This file is broken. Try picking another.',
validationFilesSizeExceeded:
'Sorry, these files are too big. You can only upload up to 1.2 GB at a time.',
},
},
mostRead: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,19 @@ describe('validateFunctions', () => {
expectedMessageCode: InvalidMessageCodes.TooManyFiles,
testMessage: 'Files uploaded exceed the expected limit of two files.',
},
{
inputRequired: false,
inputValue: [
{ name: 'hello', type: 'image/jpeg' },
{ name: 'hello', type: 'image/jpeg' },
{ name: 'hello', type: 'image/jpeg' },
],
expectedValid: false,
maxFileCount: 2,
expectedMessageCode: InvalidMessageCodes.TooManyFiles,
testMessage:
'Optional Files uploaded exceed the expected limit of two files.',
},
{
inputRequired: true,
inputValue: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ const isValidTel: (data: FieldData) => FieldData = (data: FieldData) => {
};

const isValidFiles: (data: FieldData) => FieldData = (data: FieldData) => {
const { value, required, wasInvalid, min, max, fileTypes } = data;
const { value, required, wasInvalid, min, fileTypes, max } = data;

const MAX_PAYLOAD_SIZE = 1288490189;
const RESERVED_FORM_DATA_SIZE = 10000;
Expand Down Expand Up @@ -143,13 +143,13 @@ const isValidFiles: (data: FieldData) => FieldData = (data: FieldData) => {

return { ...fileData, messageCode: fileMessageCode };
});

if (min && (value as FileData[])?.length < min && required) {
if (min != null && (value as FileData[])?.length < min && required) {
isValid = false;
messageCode = InvalidMessageCodes.NotEnoughFiles;
hasNestedErrorLabel = false;
}
if (max && (value as FileData[])?.length > max && required) {

if (max != null && (value as FileData[])?.length > max) {
isValid = false;
messageCode = InvalidMessageCodes.TooManyFiles;
hasNestedErrorLabel = false;
Expand Down
62 changes: 37 additions & 25 deletions ws-nextjs-app/pages/[service]/send/[id]/FormField/Checkbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { jsx } from '@emotion/react';
import { InputProps } from '../types';
import Label from './FieldLabel';
import styles from './styles';
import InvalidMessageBox from './InvalidMessageBox';

export default ({
id,
Expand All @@ -13,33 +14,44 @@ export default ({
label,
hasAttemptedSubmit,
}: InputProps) => {
const { isValid, value = false, required, wasInvalid } = inputState ?? {};
const {
isValid,
value = false,
required,
wasInvalid,
messageCode,
} = inputState ?? {};
const useErrorTheme = hasAttemptedSubmit && !isValid;

return (
<div css={[styles.checkboxContainer]}>
<input
css={[styles.checkbox(useErrorTheme), styles.focusIndicator]}
id={id}
name={name}
type="checkbox"
checked={value as boolean}
onChange={e => handleChange(e.target.name, e.target.checked)}
{...(!hasAttemptedSubmit && { 'aria-invalid': 'false' })}
{...(hasAttemptedSubmit && {
...(wasInvalid && { 'aria-invalid': !isValid }),
...(!isValid && { 'aria-describedby': describedBy }),
})}
{...(required && !isValid && { 'aria-required': required })}
/>
<Label
required={required}
forId={id}
css={[styles.checkboxLabel]}
useErrorTheme={useErrorTheme}
>
{label}
</Label>
</div>
<>
<div css={[styles.checkboxContainer]}>
<input
css={[styles.checkbox(useErrorTheme), styles.focusIndicator]}
id={id}
name={name}
type="checkbox"
checked={value as boolean}
onChange={e => handleChange(e.target.name, e.target.checked)}
{...(!hasAttemptedSubmit && { 'aria-invalid': 'false' })}
{...(hasAttemptedSubmit && {
...(wasInvalid && { 'aria-invalid': !isValid }),
...(!isValid && { 'aria-describedby': describedBy }),
})}
{...(required && !isValid && { 'aria-required': required })}
/>
<Label
required={required}
forId={id}
css={[styles.checkboxLabel]}
useErrorTheme={useErrorTheme}
>
{label}
</Label>
</div>
{hasAttemptedSubmit && !isValid && (
<InvalidMessageBox id={describedBy} messageCode={messageCode} />
)}
</>
);
};
12 changes: 11 additions & 1 deletion ws-nextjs-app/pages/[service]/send/[id]/FormField/EmailInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { jsx } from '@emotion/react';
import { InputProps } from '../types';
import Label from './FieldLabel';
import styles from './styles';
import InvalidMessageBox from './InvalidMessageBox';

export default ({
id,
Expand All @@ -13,7 +14,13 @@ export default ({
label,
hasAttemptedSubmit,
}: InputProps) => {
const { isValid, value = '', required, wasInvalid } = inputState ?? {};
const {
isValid,
value = '',
required,
wasInvalid,
messageCode,
} = inputState ?? {};
const useErrorTheme = hasAttemptedSubmit && !isValid;

return (
Expand All @@ -37,6 +44,9 @@ export default ({
})}
/>
</div>
{hasAttemptedSubmit && !isValid && (
<InvalidMessageBox id={describedBy} messageCode={messageCode} />
)}
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
/** @jsx jsx */
import { SetStateAction, useEffect, useState } from 'react';
import { jsx } from '@emotion/react';
import VisuallyHiddenText from '#app/components/VisuallyHiddenText';
import { useLiveRegionContext } from '#app/components/LiveRegion/LiveRegionContext';
import { FileData, InvalidMessageCodes } from '../../../types';
import { useFormContext } from '../../../FormContext';
import styles from '../styles';
import {
AUDIO_SVG_DATA_URI,
DOCUMENT_SVG_DATA_URI,
DeleteSvg,
VIDEO_SVG_DATA_URI,
} from '../svgs';
import InvalidMessageBox from '../../InvalidMessageBox';

interface FileListProps {
files: FileData[];
name: string;
hasAttemptedSubmit: boolean;
}

interface handleFileDeletionParams {
fileIndex: number;
fileName: string;
}

export default ({ files, name, hasAttemptedSubmit }: FileListProps) => {
const { handleChange } = useFormContext();
const [thumbnailState, setThumbnailState] = useState<string[]>([]);
const { replaceLiveRegionWith } = useLiveRegionContext();

const handleFileDeletion = ({
fileIndex,
fileName,
}: handleFileDeletionParams) => {
const filesClone = [...files];
filesClone.splice(fileIndex, 1);
handleChange(name, filesClone);

setThumbnailState(prevState => {
const thumbnailClone = [...prevState];
thumbnailClone.splice(fileIndex, 1);

return thumbnailClone;
});

// Needs translation
replaceLiveRegionWith(`Update, removed ${fileName}`);
};

useEffect(() => {
Promise.all(
files.map(async fileData => {
return new Promise(resolve => {
const { file } = fileData;
const fileType = file.type.substring(0, file.type.indexOf('/'));

const fileReader = new FileReader();
fileReader.onloadend = () => {
resolve(fileReader.result);
};

switch (fileType) {
case 'image':
fileReader.readAsDataURL(file);
break;
case 'video':
resolve(VIDEO_SVG_DATA_URI);
break;
case 'audio':
resolve(AUDIO_SVG_DATA_URI);
break;
default:
resolve(DOCUMENT_SVG_DATA_URI);
break;
}
});
}),
).then(result => setThumbnailState(result as SetStateAction<string[]>));
}, [files]);

const listItems = files.map((fileData: FileData, index: number) => {
const { file } = fileData;
const key = `${index}-${file.name}`;
const thumbnailSrc = thumbnailState[index];
const isThumbnailSvg = thumbnailSrc?.startsWith('data:image/svg');
const ariaDescribedById = `file-list-item-${index}`;
const errorBoxAriaDescribedById = `error-box-${index}`;
const showErrorBox = fileData.messageCode && hasAttemptedSubmit;

return (
<li css={styles.fileListItem} key={key}>
<div css={styles.fileDetails}>
<div css={styles.fileThumbnailContainer}>
<img
css={
isThumbnailSvg
? styles.fileThumbnailSvg
: styles.fileThumbnailImg
}
src={`${thumbnailSrc}`}
alt=""
/>
</div>
<span
id={ariaDescribedById}
{...(showErrorBox && {
'aria-describedby': errorBoxAriaDescribedById,
})}
>
{file.name}
</span>
<button
type="button"
aria-describedby={ariaDescribedById}
onClick={() =>
handleFileDeletion({ fileIndex: index, fileName: file.name })
}
>
<DeleteSvg />
{/* Needs translation */}
<VisuallyHiddenText>Remove</VisuallyHiddenText>
</button>
</div>

{showErrorBox && (
<InvalidMessageBox
id={errorBoxAriaDescribedById}
messageCode={fileData.messageCode as InvalidMessageCodes}
/>
)}
</li>
);
});
return (
<>
<ul css={styles.fileList}>{listItems}</ul>
</>
);
};
Loading

0 comments on commit 37e078c

Please sign in to comment.