From 660d30571fc49087495bdab5010f02d9658126df Mon Sep 17 00:00:00 2001 From: Justyn Oh Date: Thu, 15 Sep 2022 13:13:35 +0800 Subject: [PATCH 1/2] feat(a11y): improve attachment refocus on upload and remove uploads --- .../Field/Attachment/Attachment.tsx | 43 +++--------- .../Field/Attachment/AttachmentDropzone.tsx | 11 ++- .../Field/Attachment/AttachmentFileInfo.tsx | 67 +++++++++---------- 3 files changed, 50 insertions(+), 71 deletions(-) diff --git a/frontend/src/components/Field/Attachment/Attachment.tsx b/frontend/src/components/Field/Attachment/Attachment.tsx index 3b61ffe2b1..05ab69932b 100644 --- a/frontend/src/components/Field/Attachment/Attachment.tsx +++ b/frontend/src/components/Field/Attachment/Attachment.tsx @@ -1,4 +1,4 @@ -import { useCallback, useMemo } from 'react' +import { useCallback, useLayoutEffect, useMemo } from 'react' import { DropzoneProps, useDropzone } from 'react-dropzone' import { Box, @@ -94,28 +94,6 @@ export const Attachment = forwardRef( [value, readableMaxSize, showFileSize], ) - const ariaDescribedBy = useMemo(() => { - const describedByIds = new Set() - // Must be in this order so the screen reader reads out something coherent. - // 1. Label text (if available) - // 2. Initial describedby text (if available) - // 3. Max size text (if prop is true) - if (inputProps.id) { - describedByIds.add(`${inputProps.id}-label`) - } - inputProps['aria-describedby'] - ?.split(' ') - .map((id) => describedByIds.add(id)) - if (showMaxSize) { - describedByIds.add(maxSizeTextId) - } - - // Remove helptext, since label should already consist of the text - describedByIds.delete(`${inputProps.id}-helptext`) - - return Array.from(describedByIds).filter(Boolean).join(' ').trim() - }, [inputProps, maxSizeTextId, showMaxSize]) - const handleFileDrop = useCallback>( async ([acceptedFile], rejectedFiles) => { if (rejectedFiles.length > 0) { @@ -195,16 +173,13 @@ export const Attachment = forwardRef( colorScheme, }) - const handleRemoveFile = useCallback(() => { - onChange(undefined) - rootRef.current?.focus() - }, [onChange, rootRef]) + const handleRemoveFile = useCallback(() => onChange(undefined), [onChange]) // Bunch of memoization to avoid unnecessary re-renders. const processedRootProps = useMemo(() => { return getRootProps({ // Root div does not need id prop, prevents duplicate ids. - ...omit(inputProps, 'id'), + ...omit(inputProps, ['id', 'aria-describedby']), // Bunch of extra work to prevent field from being used when in readOnly // state. onKeyDown: (e) => { @@ -214,9 +189,8 @@ export const Attachment = forwardRef( } }, tabIndex: value ? -1 : 0, - 'aria-describedby': ariaDescribedBy, }) - }, [ariaDescribedBy, getRootProps, inputProps, value]) + }, [getRootProps, inputProps, value]) const processedInputProps = useMemo(() => { return getInputProps({ @@ -225,13 +199,16 @@ export const Attachment = forwardRef( }) }, [getInputProps, inputProps, name]) + useLayoutEffect(() => rootRef.current?.focus(), [rootRef, value]) + return ( {value ? ( ( )} @@ -251,6 +229,7 @@ export const Attachment = forwardRef( color="secondary.400" mt="0.5rem" textStyle="body-2" + aria-hidden > Maximum file size: {readableMaxSize} diff --git a/frontend/src/components/Field/Attachment/AttachmentDropzone.tsx b/frontend/src/components/Field/Attachment/AttachmentDropzone.tsx index ec42e3a52c..bc8f2d1447 100644 --- a/frontend/src/components/Field/Attachment/AttachmentDropzone.tsx +++ b/frontend/src/components/Field/Attachment/AttachmentDropzone.tsx @@ -1,5 +1,5 @@ import { DropzoneInputProps, DropzoneState } from 'react-dropzone' -import { chakra, Icon, Text, useStyles } from '@chakra-ui/react' +import { chakra, Icon, Text, useStyles, VisuallyHidden } from '@chakra-ui/react' import { BxsCloudUpload } from '~assets/icons/BxsCloudUpload' import Link from '~components/Link' @@ -7,23 +7,28 @@ import Link from '~components/Link' interface AttachmentDropzoneProps { inputProps: DropzoneInputProps isDragActive: DropzoneState['isDragActive'] + readableMaxSize?: string } export const AttachmentDropzone = ({ inputProps, isDragActive, + readableMaxSize, }: AttachmentDropzoneProps): JSX.Element => { const styles = useStyles() return ( <> + + Click to upload file, maximum file size of {readableMaxSize} + {isDragActive ? ( - Drop the file here ... + Drop the file here... ) : ( - + Choose file or drag and drop here diff --git a/frontend/src/components/Field/Attachment/AttachmentFileInfo.tsx b/frontend/src/components/Field/Attachment/AttachmentFileInfo.tsx index b4cf3650d1..d5f54f6436 100644 --- a/frontend/src/components/Field/Attachment/AttachmentFileInfo.tsx +++ b/frontend/src/components/Field/Attachment/AttachmentFileInfo.tsx @@ -1,6 +1,6 @@ import { useMemo } from 'react' import { BiTrash } from 'react-icons/bi' -import { Flex, forwardRef, Text, VisuallyHidden } from '@chakra-ui/react' +import { Flex, Text, VisuallyHidden } from '@chakra-ui/react' import IconButton from '~components/IconButton' @@ -11,40 +11,35 @@ export interface AttachmentFileInfoProps { handleRemoveFile: () => void } -export const AttachmentFileInfo = forwardRef( - ({ file, handleRemoveFile }, ref) => { - const readableFileSize = useMemo( - () => getReadableFileSize(file.size), - [file.size], - ) +export const AttachmentFileInfo = ({ + file, + handleRemoveFile, +}: AttachmentFileInfoProps) => { + const readableFileSize = useMemo( + () => getReadableFileSize(file.size), + [file.size], + ) - return ( - - - File attached: {file.name} with file size of {readableFileSize} - - - - {file.name} - - - {readableFileSize} - - - } - onClick={handleRemoveFile} - /> + return ( + + + File attached: {file.name} with file size of {readableFileSize} + + + + {file.name} + + + {readableFileSize} + - ) - }, -) + } + onClick={handleRemoveFile} + /> + + ) +} From 0287e00836f2682367759a80962e2a98fd9487cb Mon Sep 17 00:00:00 2001 From: Justyn Oh Date: Thu, 15 Sep 2022 15:09:57 +0800 Subject: [PATCH 2/2] fix: remove useLayoutEffect to avoid focusing on render --- frontend/src/components/Field/Attachment/Attachment.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/Field/Attachment/Attachment.tsx b/frontend/src/components/Field/Attachment/Attachment.tsx index 05ab69932b..0eb13a2367 100644 --- a/frontend/src/components/Field/Attachment/Attachment.tsx +++ b/frontend/src/components/Field/Attachment/Attachment.tsx @@ -1,4 +1,4 @@ -import { useCallback, useLayoutEffect, useMemo } from 'react' +import { useCallback, useMemo } from 'react' import { DropzoneProps, useDropzone } from 'react-dropzone' import { Box, @@ -173,7 +173,10 @@ export const Attachment = forwardRef( colorScheme, }) - const handleRemoveFile = useCallback(() => onChange(undefined), [onChange]) + const handleRemoveFile = useCallback(() => { + onChange(undefined) + rootRef.current?.focus() + }, [onChange, rootRef]) // Bunch of memoization to avoid unnecessary re-renders. const processedRootProps = useMemo(() => { @@ -199,8 +202,6 @@ export const Attachment = forwardRef( }) }, [getInputProps, inputProps, name]) - useLayoutEffect(() => rootRef.current?.focus(), [rootRef, value]) - return (