-
Notifications
You must be signed in to change notification settings - Fork 12
Feature/12088 new file upload #13229
base: develop
Are you sure you want to change the base?
Changes from 25 commits
6466949
3185823
5555f2c
f146f15
7f02322
e08c2a5
1928c3c
2a1781c
dc6e941
394ea0a
f932c14
89d0261
34e7eb8
b17514c
a458550
710db37
b3fff72
52c9918
d88580a
004c1c9
0617527
ec54034
32ba221
eadd5fe
2a8a0f0
0517200
3cae0e0
773063f
ee1388f
e94b3cb
88ab690
688822a
aa6577b
93f17f0
3d5cb87
7226177
cc6ff8a
99d338f
8dd937d
6e890de
4795f7c
822949f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,111 @@ | ||
| import React, { FC } from 'react' | ||
| import { useTranslation } from 'react-i18next' | ||
| import { Image, Pressable } from 'react-native' | ||
| import { Asset } from 'react-native-image-picker' | ||
|
|
||
| import { Icon } from '@department-of-veterans-affairs/mobile-component-library' | ||
| import styled from 'styled-components' | ||
|
|
||
| import { Box, TextView } from 'components/index' | ||
| import { NAMESPACE } from 'constants/namespaces' | ||
| import { DocumentPickerResponse } from 'screens/BenefitsScreen/BenefitsStackScreens' | ||
| import { bytesToFinalSizeDisplay, bytesToFinalSizeDisplayA11y } from 'utils/common' | ||
| import { useShowActionSheet, useTheme } from 'utils/hooks' | ||
| import { themeFn } from 'utils/theme' | ||
|
|
||
| export type FilePreviewProps = { | ||
| /** Response object from file selection with file name and size information */ | ||
| file: Asset | DocumentPickerResponse | ||
| /** Callback when the delete button is pressed */ | ||
| onDelete: () => void | ||
| /** Accessibility label for the preview element */ | ||
| a11yLabel: string | ||
| } | ||
|
|
||
| type StyledImageProps = { | ||
| /** prop to set image width */ | ||
| width: number | ||
| /** prop to set image height */ | ||
| height: number | ||
| /** Hardcoded radius of 5 due to design plan */ | ||
| borderRadius: number | ||
| } | ||
|
|
||
| const StyledImage = styled(Image)<StyledImageProps>` | ||
| width: ${themeFn<StyledImageProps>((_theme, props) => props.width)}px; | ||
| height: ${themeFn<StyledImageProps>((_theme, props) => props.height)}px; | ||
| border-radius: ${themeFn<StyledImageProps>((_theme, props) => props.borderRadius)}px; | ||
| ` | ||
|
|
||
| const FilePreview: FC<FilePreviewProps> = ({ file, onDelete, a11yLabel }) => { | ||
| const { t } = useTranslation(NAMESPACE.COMMON) | ||
| const theme = useTheme() | ||
| const confirmAlert = useShowActionSheet() | ||
|
|
||
| const isImage = !('fileCopyUri' in file) | ||
| const fileName = isImage ? file.fileName : (file as DocumentPickerResponse).name | ||
| const fileSize = isImage ? file.fileSize : (file as DocumentPickerResponse).size | ||
| const imageUri = isImage ? file.uri : undefined | ||
| const fileSizeDisplayText = fileSize ? bytesToFinalSizeDisplay(fileSize, t, false) : '' | ||
| const fileSizeA11yText = fileSize ? bytesToFinalSizeDisplayA11y(fileSize, t, false) : '' | ||
| const accessibilityLabel = a11yLabel?.concat(fileName || '', fileSizeA11yText) | ||
|
|
||
| const onPress = (): void => { | ||
| const options = [t('remove'), t('keep')] | ||
|
|
||
| confirmAlert( | ||
| { | ||
| options, | ||
| title: t('file.removeFile'), | ||
| destructiveButtonIndex: 0, | ||
| }, | ||
| (buttonIndex) => { | ||
| switch (buttonIndex) { | ||
| case 0: | ||
| onDelete() | ||
| break | ||
| case 1: | ||
| break | ||
| } | ||
| }, | ||
| ) | ||
| } | ||
|
|
||
| return ( | ||
| <Pressable | ||
| onPress={onPress} | ||
| accessibilityRole="button" | ||
| accessibilityLabel={accessibilityLabel} | ||
| accessibilityHint={t('fileUpload.delete.a11yHint')}> | ||
| <Box flexDirection="row" gap={10}> | ||
| {imageUri ? ( | ||
| <StyledImage | ||
| source={{ uri: imageUri }} | ||
| width={theme.dimensions.imagePreviewWidth} | ||
| height={theme.dimensions.imagePreviewWidth} | ||
| borderRadius={5} | ||
| /> | ||
| ) : ( | ||
| <Box width={theme.dimensions.imagePreviewWidth}> | ||
| <Icon | ||
| name={'DescriptionOutlined'} | ||
| fill={theme.colors.text.primary} | ||
| width={theme.dimensions.filePreviewWidth} | ||
| height={theme.dimensions.filePreviewWidth} | ||
| preventScaling={true} | ||
| /> | ||
| </Box> | ||
| )} | ||
| <Box flex={1}> | ||
| <TextView variant="HelperTextBold">{fileName}</TextView> | ||
| <TextView variant="HelperText">{fileSizeDisplayText}</TextView> | ||
| </Box> | ||
| <Box> | ||
| <Icon name={'Delete'} fill={theme.colors.icon.error} width={24} height={25} preventScaling={true} /> | ||
| </Box> | ||
| </Box> | ||
| </Pressable> | ||
| ) | ||
| } | ||
|
|
||
| export default FilePreview | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,146 @@ | ||
| import React, { FC, useMemo } from 'react' | ||
| import { useTranslation } from 'react-i18next' | ||
| import { Asset } from 'react-native-image-picker' | ||
|
|
||
| import { Button, ButtonVariants, Icon } from '@department-of-veterans-affairs/mobile-component-library' | ||
|
|
||
| import FilePreview from 'components/FileUpload/FilePreview' | ||
| import { Box, BoxProps, TextView } from 'components/index' | ||
| import { NAMESPACE } from 'constants/namespaces' | ||
| import { DocumentPickerResponse } from 'screens/BenefitsScreen/BenefitsStackScreens' | ||
| import theme from 'styles/themes/standardTheme' | ||
| import { bytesToFinalSizeDisplay, bytesToFinalSizeDisplayA11y } from 'utils/common' | ||
| import { getFileUris, getImageBase64s, getTotalBytesUsedByFiles } from 'utils/fileUpload' | ||
| import { useShowActionSheet } from 'utils/hooks' | ||
| import { onAddMultipleFileAttachments } from 'utils/secureMessaging' | ||
|
|
||
| export type FileUploadProps = { | ||
| /** i18n key for the text label next to the file upload field */ | ||
| labelKey?: string | ||
| /** List of Assets or DocumentPickerResponse objects to display */ | ||
| fileList: Array<Asset | DocumentPickerResponse> | ||
| /** Callback after a successful file selection is made */ | ||
| onFileSelection: (response: Array<Asset | DocumentPickerResponse>) => void | ||
| /** Optional callback when a file's delete button is pressed */ | ||
| onDeleteFile?: (idx: number) => void | ||
| /** Callback to return an error message after an invalid file selection */ | ||
| setError: (value: string) => void | ||
| /** Callback to return an a11y-friendly error message after an invalid file selection */ | ||
| setErrorA11y: (value: string) => void | ||
| } | ||
|
|
||
| const FileUpload: FC<FileUploadProps> = ({ | ||
| labelKey, | ||
| fileList, | ||
| onFileSelection, | ||
| onDeleteFile, | ||
| setError, | ||
| setErrorA11y, | ||
| }) => { | ||
| const showActionSheetWithOptions = useShowActionSheet() | ||
| const { t } = useTranslation(NAMESPACE.COMMON) | ||
|
|
||
| const totalBytesUsed = useMemo(() => getTotalBytesUsedByFiles(fileList), [fileList]) | ||
|
|
||
| const containerProps: BoxProps = { | ||
| borderColor: 'photoAdd', | ||
| borderWidth: 2, | ||
| borderStyle: 'dashed', | ||
| borderRadius: 5, | ||
| gap: 16, | ||
| px: 20, | ||
| py: 16, | ||
| } | ||
|
|
||
| const onChooseAFile = (): void => { | ||
| // logAnalyticsEvent(Events.vama_sm_attach('Select a file')) | ||
| onAddMultipleFileAttachments( | ||
| t, | ||
| showActionSheetWithOptions, | ||
| setError, | ||
| setErrorA11y, | ||
| onFileSelection, | ||
| getTotalBytesUsedByFiles(fileList), | ||
| getFileUris(fileList), | ||
| getImageBase64s(fileList), | ||
| ) | ||
|
AdryienH marked this conversation as resolved.
|
||
| } | ||
|
|
||
| const getFileList = () => { | ||
| if (fileList.length < 1) { | ||
| return ( | ||
| <Box alignItems="center"> | ||
| <Icon name={'UploadFile'} width={56} height={56} preventScaling={true} /> | ||
| </Box> | ||
| ) | ||
| } | ||
|
|
||
| return ( | ||
| <> | ||
| {fileList.map((file, idx) => { | ||
| return ( | ||
| <FilePreview | ||
| file={file} | ||
| onDelete={() => onDeleteFile?.(idx)} | ||
| a11yLabel={t('fileUpload.ofTotalFiles', { | ||
|
Comment on lines
+79
to
+85
|
||
| fileNum: idx + 1, | ||
| count: fileList.length, | ||
| })} | ||
| /> | ||
| ) | ||
| })} | ||
| <Box | ||
| flexDirection="row" | ||
| borderTopColor="divider" | ||
| justifyContent="space-between" | ||
| borderTopWidth={2} | ||
| pt={8} | ||
| px={20}> | ||
| <TextView variant="HelperText" color={'fileInfo'}> | ||
| {t('fileUpload.ofTenFiles', { numOfFiles: fileList?.length })} | ||
| </TextView> | ||
| {/*eslint-disable-next-line react-native-a11y/has-accessibility-hint*/} | ||
| <TextView | ||
| variant="HelperText" | ||
| color={'fileInfo'} | ||
| accessibilityLabel={t('fileUpload.ofFiftyMB.a11y', { | ||
| sizeOfPhotos: bytesToFinalSizeDisplayA11y(totalBytesUsed ? totalBytesUsed : 0, t, false), | ||
| })}> | ||
| {t('fileUpload.ofFiftyMB', { | ||
| sizeOfPhotos: bytesToFinalSizeDisplay(totalBytesUsed ? totalBytesUsed : 0, t, false), | ||
| })} | ||
| </TextView> | ||
|
Comment on lines
+99
to
+112
|
||
| </Box> | ||
| </> | ||
| ) | ||
| } | ||
|
|
||
| return ( | ||
| <> | ||
| {labelKey && ( | ||
| <Box display="flex" flexDirection="row" flexWrap="wrap" mb={theme.dimensions.smallMarginBetween}> | ||
| <TextView>{t(labelKey)}</TextView> | ||
| </Box> | ||
| )} | ||
| <Box {...containerProps}> | ||
| <Box> | ||
| {/*eslint-disable-next-line react-native-a11y/has-accessibility-hint*/} | ||
| <TextView | ||
| variant="HelperText" | ||
| color={'placeholder'} | ||
| accessibilityLabel={t('fileUpload.acceptedFileSpecs.a11y')}> | ||
| {t('fileUpload.acceptedFileSpecs')} | ||
| </TextView> | ||
| </Box> | ||
| {getFileList()} | ||
| <Button | ||
| label={fileList.length < 1 ? t('fileUpload.chooseAFile') : t('fileUpload.attachFiles')} | ||
| onPress={onChooseAFile} | ||
| buttonType={ButtonVariants.Secondary} | ||
| /> | ||
| </Box> | ||
| </> | ||
| ) | ||
| } | ||
|
|
||
| export default FileUpload | ||
|
AdryienH marked this conversation as resolved.
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
accessibilityLabelis built viaconcat(fileName, fileSizeA11yText)without any spacing/punctuation, which will be read as a single run-on string by screen readers.Suggested fix: build the label with explicit separators (e.g., commas/spaces) and consider including the file type as well for clarity.