in src/file-uploader/file-uploader.tsx [46:632]
export default function FileUploader(props: FileUploaderProps) {
if (props['onDrop']) {
console.error('onDrop is not a prop for FileUploader.');
}
if (props['onDropAccepted']) {
console.error('onDropAccepted is not a prop for FileUploader.');
}
if (props['onDropRejected']) {
console.error('onDropRejected is not a prop for FileUploader.');
}
if (props['progressAmount']) {
console.error('progressAmount is not a prop for FileUploader.');
}
// Isolate props that are not meant to be passed to FileUploaderBasic
const {
fileRows,
hint,
itemPreview,
label,
maxFiles,
overrides = {},
processFileOnDrop,
progressAmountStartValue,
setFileRows,
...fileUploaderBasicProps
} = props;
// Isolate styles that are not meant to be passed to FileUploaderBasic
const {
// Overrides for FileUploader
CircleCheckFilledIcon: OverridesCircleCheckFilledIcon,
CircleExclamationPointFilledIcon: OverridesCircleExclamationPointFilledIcon,
DeleteButtonComponent: OverridesDeleteButtonComponent,
FileRow: OverridesFileRow,
FileRowColumn: OverridesFileRowColumn,
FileRowContent: OverridesFileRowContent,
FileRowFileName: OverridesFileRowFileName,
FileRowText: OverridesFileRowText,
FileRowUploadMessage: OverridesFileRowUploadMessage,
FileRowUploadText: OverridesFileRowUploadText,
FileRows: OverridesFileRows,
Hint: OverridesHint,
ImagePreviewThumbnail: OverridesImagePreviewThumbnail,
ItemPreviewContainer: OverridesItemPreviewContainer,
Label: OverridesLabel,
PaperclipFilledIcon: OverridesPaperclipFilledIcon,
ParentRoot: OverridesParentRoot,
TrashCanFilledIcon: OverridesTrashCanFilledIcon,
// Overrides for FileUploaderBasic that are modified in this file
ButtonComponent,
ContentMessage,
FileDragAndDrop,
...fileUploaderBasicOverrides
} = overrides;
const [css, theme] = useStyletron();
// Prepare icon overrides
const [CircleCheckFilledIcon, circleCheckFilledIconProps] = getOverrides(
overrides.CircleCheckFilledIcon,
CircleCheckFilledIconComponent
);
const [CircleExclamationPointFilledIcon, circleExclamationPointFilledIconProps] = getOverrides(
OverridesCircleExclamationPointFilledIcon,
CircleExclamationPointFilled
);
const [PaperclipFilledIcon, paperclipFilledIconProps] = getOverrides(
OverridesPaperclipFilledIcon,
PaperclipFilledIconComponent
);
const [TrashCanFilledIcon, trashCanFilledIconProps] = getOverrides(
OverridesTrashCanFilledIcon,
TrashCanFilledIconComponent
);
// Prepare baseui component overrides
const [DeleteButtonComponent, deleteButtonProps] = getOverrides(
OverridesDeleteButtonComponent,
Button
);
const [ProgressBarComponent, progressBarProps] = getOverrides(overrides.ProgressBar, ProgressBar);
// Prepare styled component overrides
const [FileRow, fileRowProps] = getOverrides(OverridesFileRow, StyledFileRow);
const [FileRowColumn, fileRowColumnProps] = getOverrides(
OverridesFileRowColumn,
StyledFileRowColumn
);
const [FileRowContent, fileRowContentProps] = getOverrides(
OverridesFileRowContent,
StyledFileRowContent
);
const [FileRowFileName, fileRowFileNameProps] = getOverrides(
OverridesFileRowFileName,
StyledFileRowFileName
);
const [FileRowText, fileRowTextProps] = getOverrides(OverridesFileRowText, StyledFileRowText);
const [FileRowUploadMessage, fileRowUploadMessageProps] = getOverrides(
OverridesFileRowUploadMessage,
StyledFileRowUploadMessage
);
const [FileRowUploadText, fileRowUploadTextProps] = getOverrides(
OverridesFileRowUploadText,
StyledFileRowUploadText
);
const [FileRows, fileRowsProps] = getOverrides(OverridesFileRows, StyledFileRows);
const [Hint, hintProps] = getOverrides(OverridesHint, StyledHint);
const [ImagePreviewThumbnail, imagePreviewThumbnailProps] = getOverrides(
OverridesImagePreviewThumbnail,
StyledImagePreviewThumbnail
);
const [ItemPreviewContainer, itemPreviewContainerProps] = getOverrides(
OverridesItemPreviewContainer,
StyledItemPreviewContainer
);
const [Label, labelProps] = getOverrides(OverridesLabel, StyledLabel);
const [ParentRoot, parentRootProps] = getOverrides(OverridesParentRoot, StyledParentRoot);
const onDrop = React.useCallback(
(acceptedFiles: Array<File>, rejectedFiles: Array<File>) => {
const newFiles = acceptedFiles.concat(rejectedFiles);
let newFileRows = [...props.fileRows];
newFiles.forEach((file: File) => {
newFileRows.push({
errorMessage: null,
file,
id: uid(file),
imagePreviewThumbnail: '',
progressAmount: progressAmountStartValue ?? PROGRESS_AMOUNT_LOADING,
status: FILE_STATUS.added,
});
props.setFileRows([...newFileRows]);
});
newFileRows.forEach((fileRow: FileRow, index: number) => {
if (fileRow.status === FILE_STATUS.added) {
let reader = new FileReader();
reader.onerror = () => {
newFileRows[index].errorMessage = 'cannot read file';
newFileRows[index].status = FILE_STATUS.error;
newFileRows[index].progressAmount = PROGRESS_AMOUNT_LOADING_COMPLETE;
handleAriaLiveUpdates(
ARIA_LIVE_ELEMENT_ID.ADDITION,
`${newFileRows[index].file.name} added, upload failed: ${newFileRows[index].errorMessage}`
);
props.setFileRows([...newFileRows]);
};
reader.onload = (event) => {
if (newFileRows[index].file.type.startsWith('image/')) {
newFileRows[index].imagePreviewThumbnail = event.target?.result;
props.setFileRows([...newFileRows]);
}
if (
props.maxFiles !== undefined &&
Number.isInteger(props.maxFiles) &&
index >= props.maxFiles
) {
// If too many files
newFileRows[
index
].errorMessage = `cannot process more than ${props.maxFiles} file(s)`;
newFileRows[index].status = FILE_STATUS.error;
newFileRows[index].progressAmount = PROGRESS_AMOUNT_LOADING_COMPLETE;
handleAriaLiveUpdates(
ARIA_LIVE_ELEMENT_ID.ADDITION,
`${newFileRows[index].file.name} added, upload failed: ${newFileRows[index].errorMessage}`
);
props.setFileRows([...newFileRows]);
} else if (
props.minSize !== undefined &&
Number.isInteger(props.minSize) &&
props.minSize > fileRow.file.size
) {
// If file size is too small
newFileRows[index].errorMessage = `file size must be greater than ${formatBytes(
props.minSize
)}`;
newFileRows[index].status = FILE_STATUS.error;
newFileRows[index].progressAmount = PROGRESS_AMOUNT_LOADING_COMPLETE;
handleAriaLiveUpdates(
ARIA_LIVE_ELEMENT_ID.ADDITION,
`${newFileRows[index].file.name} added, upload failed: ${newFileRows[index].errorMessage}`
);
props.setFileRows([...newFileRows]);
} else if (
props.maxSize !== undefined &&
Number.isInteger(props.maxSize) &&
props.maxSize < fileRow.file.size
) {
// If file size is too big
newFileRows[index].errorMessage = `file size must be less than ${formatBytes(
props.maxSize
)}`;
newFileRows[index].status = FILE_STATUS.error;
newFileRows[index].progressAmount = PROGRESS_AMOUNT_LOADING_COMPLETE;
handleAriaLiveUpdates(
ARIA_LIVE_ELEMENT_ID.ADDITION,
`${newFileRows[index].file.name} added, upload failed: ${newFileRows[index].errorMessage}`
);
props.setFileRows([...newFileRows]);
} else if (index >= newFileRows.length - rejectedFiles.length) {
// If file was rejected by dropzone (e.g. wrong file type)
newFileRows[index].errorMessage = fileRow.file.type
? `file type of ${fileRow.file.type} is not accepted`
: 'file type is not accepted';
newFileRows[index].status = FILE_STATUS.error;
newFileRows[index].progressAmount = PROGRESS_AMOUNT_LOADING_COMPLETE;
handleAriaLiveUpdates(
ARIA_LIVE_ELEMENT_ID.ADDITION,
`${newFileRows[index].file.name} added, upload failed: ${newFileRows[index].errorMessage}`
);
props.setFileRows([...newFileRows]);
} else if (props.processFileOnDrop) {
// If caller passed in file process function
props
.processFileOnDrop(fileRow.file, fileRow.id, newFileRows)
.then(
({ errorMessage, fileInfo }: { errorMessage: string | null; fileInfo?: any }) => {
if (fileInfo) {
newFileRows[index].fileInfo = fileInfo;
}
if (errorMessage) {
newFileRows[index].errorMessage = errorMessage;
newFileRows[index].status = FILE_STATUS.error;
newFileRows[index].progressAmount = PROGRESS_AMOUNT_LOADING_COMPLETE;
} else {
newFileRows[index].status = FILE_STATUS.processed;
newFileRows[index].progressAmount = PROGRESS_AMOUNT_LOADING_COMPLETE;
}
}
)
.catch((error) => {
console.error('error with processFileOnDrop', error);
newFileRows[index].errorMessage = 'unknown processing error';
newFileRows[index].status = FILE_STATUS.error;
newFileRows[index].progressAmount = PROGRESS_AMOUNT_LOADING_COMPLETE;
handleAriaLiveUpdates(
ARIA_LIVE_ELEMENT_ID.ADDITION,
`${newFileRows[index].file.name} added, upload failed: ${newFileRows[index].errorMessage}`
);
})
.finally(() => {
props.setFileRows([...newFileRows]);
});
} else {
// If no errors and no file process function
newFileRows[index].status = FILE_STATUS.processed;
newFileRows[index].progressAmount = PROGRESS_AMOUNT_LOADING_COMPLETE;
handleAriaLiveUpdates(
ARIA_LIVE_ELEMENT_ID.ADDITION,
`${newFileRows[index].file.name} added, upload successful`
);
props.setFileRows([...newFileRows]);
}
};
reader.readAsDataURL(fileRow.file);
}
});
},
[props]
);
const removeFileRow = (event: React.MouseEvent) => {
event.preventDefault();
const indexOfFileRowToRemove = Number(event?.currentTarget?.getAttribute('index'));
handleAriaLiveUpdates(
ARIA_LIVE_ELEMENT_ID.REMOVAL,
`${props.fileRows[indexOfFileRowToRemove].file.name} removed`
);
props.setFileRows([...props.fileRows.toSpliced(indexOfFileRowToRemove, 1)]);
const label = document.querySelector('[data-baseweb="file-uploader-label"]') as HTMLElement;
if (label) {
label.focus();
}
};
const noFilesAreLoading = React.useMemo(
() => !props.fileRows.find((fileRow: FileRow) => fileRow.status === FILE_STATUS.added),
[props.fileRows]
);
return (
<LocaleContext.Consumer>
{(locale) => (
<ParentRoot data-baseweb="file-uploader-parent-root" {...parentRootProps}>
<span
aria-live="assertive"
aria-relevant="additions"
className={css({
top: 0,
left: '-4px',
width: '1px',
height: '1px',
position: 'absolute',
overflow: 'hidden',
})}
id={ARIA_LIVE_ELEMENT_ID.ADDITION}
></span>
<span
aria-live="polite"
aria-relevant="additions"
className={css({
top: 0,
left: '-2px',
width: '1px',
height: '1px',
position: 'absolute',
overflow: 'hidden',
})}
id={ARIA_LIVE_ELEMENT_ID.REMOVAL}
></span>
{props.label && (
<Label
data-baseweb="file-uploader-label"
tabIndex={-1}
{...labelProps}
$disabled={!!props.disabled}
>
{props.label}
</Label>
)}
<FileUploaderBasic
buttonIcon={() => <Upload aria-hidden={'true'} />}
buttonText={locale.fileuploader.buttonText}
contentMessage={locale.fileuploader.contentMessage}
overrides={{
ButtonComponent: {
props: {
'aria-describedby': 'file-uploader-hint',
shape: SHAPE.default,
size: SIZE.default,
...props.overrides?.ButtonComponent?.props,
style: {
marginTop: 0,
...destructureStyleOverride(
// @ts-expect-error
props.overrides?.ButtonComponent?.props?.style,
theme
),
},
overrides: {
// @ts-expect-error
...props.overrides?.ButtonComponent?.props?.overrides,
BaseButton: {
// @ts-expect-error
...props.overrides?.ButtonComponent?.props?.overrides?.BaseButton,
style: {
backgroundColor: theme.colors.backgroundPrimary,
height: theme.sizing.scale950,
display: 'flex',
flexDirection: 'row',
gap: '8px',
...theme.typography.LabelSmall,
...destructureStyleOverride(
// @ts-expect-error
props.overrides?.ButtonComponent?.props?.overrides?.BaseButton?.style,
theme
),
},
},
},
},
},
ContentMessage: {
style: {
...theme.typography.ParagraphSmall,
color: theme.colors.contentTertiary,
...destructureStyleOverride(props.overrides?.ContentMessage?.style, theme),
},
},
FileDragAndDrop: {
style: (fileDragAndDropProps) => ({
backgroundColor: fileDragAndDropProps.$theme.colors.fileUploaderBackgroundColor,
borderColor: fileDragAndDropProps.$isDragActive
? fileDragAndDropProps.$theme.colors.borderSelected
: fileDragAndDropProps.$theme.colors.fileUploaderBorderColorDefault,
borderStyle: 'solid',
borderWidth: '3px',
flexDirection: 'row',
flexWrap: 'wrap',
gap: theme.sizing.scale300,
paddingBottom: theme.sizing.scale600,
paddingLeft: theme.sizing.scale600,
paddingRight: theme.sizing.scale600,
paddingTop: theme.sizing.scale600,
...destructureStyleOverride(props.overrides?.FileDragAndDrop?.style, theme),
}),
},
Root: {
style: {
marginBottom: theme.sizing.scale300,
...destructureStyleOverride(props.overrides?.Root?.style, theme),
},
},
...fileUploaderBasicOverrides,
}}
swapButtonAndMessage
{...fileUploaderBasicProps}
// Disable uploads while files are loading, even if application passes disabled as false
disabled={
!!props.disabled
? props.disabled
: !!props.fileRows.find((fileRow: FileRow) => fileRow.status === FILE_STATUS.added)
}
// Implement or use no-op callbacks to prevent consumers from passing them in
onDrop={onDrop}
onDropAccepted={(_: Array<File>) => {}}
onDropRejected={(_: Array<File>) => {}}
progressAmount={undefined}
/>
{props.fileRows.length > 0 && (
<FileRows data-baseweb="file-uploader-file-rows" {...fileRowsProps}>
{props.fileRows.map((fileRow, index) => (
<FileRow
id={`file-uploader-file-row-${index}`}
data-baseweb="file-uploader-file-row"
key={fileRow.id}
{...fileRowProps}
>
{props.itemPreview && (
<ItemPreviewContainer
aria-hidden={'true'}
data-baseweb="file-uploader-item-preview-container"
{...itemPreviewContainerProps}
>
{fileRow.imagePreviewThumbnail ? (
<ImagePreviewThumbnail
alt={fileRow.file.name}
data-baseweb="file-uploader-image-preview-thumbnail"
src={fileRow.imagePreviewThumbnail}
{...imagePreviewThumbnailProps}
/>
) : (
<PaperclipFilledIcon
data-baseweb="file-uploader-paperclip-filled-icon"
color={theme.colors.contentSecondary}
{...paperclipFilledIconProps}
/>
)}
</ItemPreviewContainer>
)}
<FileRowColumn
data-baseweb="file-uploader-file-row-column"
{...fileRowColumnProps}
>
<FileRowContent
data-baseweb="file-uploader-file-row-content"
{...fileRowContentProps}
>
<FileRowText data-baseweb="file-uploader-file-row-text" {...fileRowTextProps}>
<FileRowFileName
data-baseweb="file-uploader-file-row-file-name"
{...fileRowFileNameProps}
>
{fileRow.file.name}
</FileRowFileName>
<FileRowUploadMessage
data-baseweb="file-uploader-file-row-upload-message"
$color={FILE_STATUS_TO_COLOR_MAP(theme)[fileRow.status]}
{...fileRowUploadMessageProps}
>
{fileRow.status === FILE_STATUS.error && (
<>
<CircleExclamationPointFilledIcon
aria-hidden={'true'}
color={FILE_STATUS_TO_COLOR_MAP(theme)[fileRow.status]}
data-baseweb="file-uploader-circle-exclamation-point-filled-icon"
title={fileRow.status}
{...circleExclamationPointFilledIconProps}
/>
<FileRowUploadText
aria-errormessage={fileRow.errorMessage}
aria-invalid={true}
data-baseweb="file-uploader-file-row-upload-message-text"
id={`file-uploader-file-row-upload-message-text-${index}`}
{...fileRowUploadTextProps}
>
{locale.fileuploader.error}
{fileRow.errorMessage}
</FileRowUploadText>
</>
)}
{fileRow.status === FILE_STATUS.processed && (
<>
<CircleCheckFilledIcon
aria-hidden={'true'}
color={FILE_STATUS_TO_COLOR_MAP(theme)[fileRow.status]}
data-baseweb="file-uploader-circle-check-filled-icon"
title={fileRow.status}
{...circleCheckFilledIconProps}
/>
<FileRowUploadText
data-baseweb="file-uploader-file-row-upload-message-text"
{...fileRowUploadTextProps}
>
{locale.fileuploader.processed}
</FileRowUploadText>
</>
)}
{fileRow.status === FILE_STATUS.added && (
<FileRowUploadText
$color={theme.colors.contentTertiary}
data-baseweb="file-uploader-file-row-upload-message-text"
{...fileRowUploadTextProps}
>
{locale.fileuploader.added}
</FileRowUploadText>
)}
</FileRowUploadMessage>
</FileRowText>
{noFilesAreLoading && (
<DeleteButtonComponent
aria-label={`Remove ${fileRow.file.name}`}
data-baseweb="file-uploader-delete-button-component"
index={index}
onClick={removeFileRow}
kind={KIND.tertiary}
shape={SHAPE.circle}
size={SIZE.compact}
{...deleteButtonProps}
>
<TrashCanFilledIcon
aria-hidden={'true'}
data-baseweb="file-uploader-trash-can-filled-icon"
overrides={{ Svg: { style: { verticalAlign: 'middle' } } }}
size={theme.sizing.scale600}
title={'remove'}
{...trashCanFilledIconProps}
/>
</DeleteButtonComponent>
)}
</FileRowContent>
<ProgressBarComponent
aria-hidden={'true'}
data-baseweb="file-uploader-progress-bar"
overrides={{
Bar: {
style: {
marginTop: theme.sizing.scale0,
marginBottom: theme.sizing.scale0,
marginLeft: 0,
marginRight: 0,
},
},
BarContainer: {
style: {
marginTop: 0,
marginBottom: 0,
marginLeft: 0,
marginRight: 0,
},
},
BarProgress: {
// @ts-ignore
style: ({ $theme }) => ({
backgroundColor: FILE_STATUS_TO_COLOR_MAP($theme)[fileRow.status],
}),
},
}}
size={PROGRESS_BAR_SIZE.small}
value={fileRow.progressAmount ?? PROGRESS_AMOUNT_LOADING_COMPLETE}
{...progressBarProps}
/>
</FileRowColumn>
</FileRow>
))}
</FileRows>
)}
{props.hint && (
<Hint
data-baseweb="file-uploader-hint"
id="file-uploader-hint"
$fileCount={props.fileRows.length}
{...hintProps}
>
{props.hint}
</Hint>
)}
</ParentRoot>
)}
</LocaleContext.Consumer>
);
}