@carbon/react
Version:
React components for the Carbon Design System
359 lines (351 loc) • 12.1 kB
JavaScript
/**
* Copyright IBM Corp. 2016, 2023
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
var _rollupPluginBabelHelpers = require('../../_virtual/_rollupPluginBabelHelpers.js');
var cx = require('classnames');
var PropTypes = require('prop-types');
var React = require('react');
var Filename = require('./Filename.js');
var FileUploaderButton = require('./FileUploaderButton.js');
var Button = require('../Button/Button.js');
var keys = require('../../internal/keyboard/keys.js');
var match = require('../../internal/keyboard/match.js');
var usePrefix = require('../../internal/usePrefix.js');
var Text = require('../Text/Text.js');
require('../Text/TextDirection.js');
var useId = require('../../internal/useId.js');
var index = require('../FeatureFlags/index.js');
const FileUploader = /*#__PURE__*/React.forwardRef(({
accept,
buttonKind,
buttonLabel,
className,
disabled,
filenameStatus,
iconDescription,
labelDescription,
labelTitle,
maxFileSize,
multiple,
name,
onAddFiles,
onChange,
onClick,
onDelete,
size,
...other
}, ref) => {
const fileUploaderInstanceId = useId.useId('file-uploader');
const prefix = usePrefix.usePrefix();
const enhancedFileUploaderEnabled = index.useFeatureFlag('enable-enhanced-file-uploader');
const [fileItems, setFileItems] = React.useState([]);
const [legacyFileNames, setLegacyFileNames] = React.useState([]);
const uploaderButton = /*#__PURE__*/React.createRef();
const nodes = [];
const createFileItem = React.useCallback(file => ({
name: file.name,
uuid: `${fileUploaderInstanceId}-${Date.now()}-${Array.from(crypto.getRandomValues(new Uint8Array(8))).map(b => b.toString(36)).join('')}`,
file
}), [fileUploaderInstanceId]);
/**
* Validates files based on file size restrictions.
* Marks invalid files with `invalidFileType: true` but includes them in the result.
*
* Note: The `accept` prop is passed to the native HTML input element (`FileUploaderButton`),
* which provides UI-level filtering in the file picker dialog, but there is no JavaScript validation
* for file types - users can bypass this by changing the file type filter in the dialog.
* https://github.com/carbon-design-system/carbon/issues/21166
*/
const validateFiles = React.useCallback(files => {
return files.map(file => {
if (maxFileSize && file.size > maxFileSize) {
file.invalidFileType = true;
}
return file;
});
}, [maxFileSize]);
const handleChange = React.useCallback(evt => {
evt.stopPropagation();
const incomingFiles = Array.from(evt.target.files);
const filesToValidate = multiple ? incomingFiles : [incomingFiles[0]];
const validatedFiles = validateFiles(filesToValidate);
if (onAddFiles) {
onAddFiles(evt, {
addedFiles: validatedFiles
});
}
// Filter out invalid files since FileUploader cannot display them
// (FileUploaderDropContainer returns all files because parent uses FileUploaderItem to display errors)
// https://github.com/carbon-design-system/carbon/issues/21166
const validFiles = validatedFiles.filter(file => !file.invalidFileType);
if (validFiles.length === 0) {
return;
}
if (enhancedFileUploaderEnabled) {
const newFileItems = validFiles.map(createFileItem);
let updatedFileItems;
if (multiple) {
const existingNames = new Set(fileItems.map(item => item.name));
const uniqueNewItems = newFileItems.filter(item => !existingNames.has(item.name));
updatedFileItems = [...fileItems, ...uniqueNewItems];
} else {
updatedFileItems = newFileItems;
}
setFileItems(updatedFileItems);
if (onChange) {
const allFiles = updatedFileItems.map(item => item.file);
const enhancedEvent = {
...evt,
target: {
...evt.target,
files: Object.assign(allFiles, {
item: index => allFiles[index] || null
}),
addedFiles: newFileItems,
currentFiles: updatedFileItems,
action: 'add'
}
};
onChange(enhancedEvent);
}
} else {
const filenames = validFiles.map(file => file.name);
const updatedFileNames = multiple ? [...new Set([...legacyFileNames, ...filenames])] : filenames;
setLegacyFileNames(updatedFileNames);
if (onChange) {
onChange(evt);
}
}
}, [enhancedFileUploaderEnabled, fileItems, legacyFileNames, multiple, onAddFiles, onChange, createFileItem, validateFiles]);
const handleClick = React.useCallback((evt, {
index,
filenameStatus
}) => {
if (filenameStatus === 'edit') {
evt.stopPropagation();
if (enhancedFileUploaderEnabled) {
const deletedItem = fileItems[index];
if (!deletedItem) return;
const remainingItems = fileItems.filter((_, i) => i !== index);
setFileItems(remainingItems);
const remainingFiles = remainingItems.map(item => item.file);
const enhancedEvent = {
...evt,
target: {
...evt.target,
files: Object.assign(remainingFiles, {
item: index => remainingFiles[index] || null
}),
deletedFile: deletedItem,
deletedFileName: deletedItem.name,
remainingFiles: remainingItems,
currentFiles: remainingItems,
action: 'remove'
}
};
if (onDelete) {
onDelete(enhancedEvent);
}
if (onChange) {
onChange(enhancedEvent);
}
} else {
const deletedFileName = legacyFileNames[index];
const filteredArray = legacyFileNames.filter(filename => filename !== deletedFileName);
setLegacyFileNames(filteredArray);
if (onDelete) {
onDelete(evt);
}
}
if (onClick) {
onClick(evt);
}
uploaderButton.current?.focus?.();
}
}, [enhancedFileUploaderEnabled, fileItems, legacyFileNames, onDelete, onChange, onClick, uploaderButton]);
React.useImperativeHandle(ref, () => ({
clearFiles() {
if (enhancedFileUploaderEnabled) {
const previousItems = [...fileItems];
setFileItems([]);
if (onChange && previousItems.length > 0) {
const enhancedEvent = {
target: {
files: Object.assign([], {
item: () => null
}),
clearedFiles: previousItems,
currentFiles: [],
action: 'clear'
},
preventDefault: () => {},
stopPropagation: () => {}
};
onChange(enhancedEvent);
}
} else {
setLegacyFileNames([]);
}
},
...(enhancedFileUploaderEnabled && {
getCurrentFiles() {
return [...fileItems];
}
})
}), [enhancedFileUploaderEnabled, fileItems, onChange]);
const classes = cx({
[`${prefix}--form-item`]: true,
[className]: className
});
const getHelperLabelClasses = baseClass => cx(baseClass, {
[`${prefix}--label-description--disabled`]: disabled
});
const selectedFileClasses = cx(`${prefix}--file__selected-file`, {
[`${prefix}--file__selected-file--md`]: size === 'field' || size === 'md',
[`${prefix}--file__selected-file--sm`]: size === 'small' || size === 'sm'
});
const displayFiles = enhancedFileUploaderEnabled ? fileItems.map((item, index) => ({
name: item.name,
key: item.uuid,
index
})) : legacyFileNames.map((name, index) => ({
name,
key: index,
index
}));
return /*#__PURE__*/React.createElement("div", _rollupPluginBabelHelpers.extends({
className: classes
}, other), !labelTitle ? null : /*#__PURE__*/React.createElement(Text.Text, {
as: "h3",
className: getHelperLabelClasses(`${prefix}--file--label`)
}, labelTitle), /*#__PURE__*/React.createElement(Text.Text, {
as: "p",
className: getHelperLabelClasses(`${prefix}--label-description`),
id: fileUploaderInstanceId
}, labelDescription), /*#__PURE__*/React.createElement(FileUploaderButton.default, {
innerRef: uploaderButton,
disabled: disabled,
labelText: buttonLabel,
multiple: multiple,
buttonKind: buttonKind,
onChange: handleChange,
disableLabelChanges: true,
accept: accept,
name: name,
size: size,
"aria-describedby": fileUploaderInstanceId
}), /*#__PURE__*/React.createElement("div", {
className: `${prefix}--file-container`
}, displayFiles.length === 0 ? null : displayFiles.map(file => /*#__PURE__*/React.createElement("span", _rollupPluginBabelHelpers.extends({
key: file.key,
className: selectedFileClasses,
ref: node => {
nodes[file.index] = node;
}
}, other), /*#__PURE__*/React.createElement(Text.Text, {
as: "p",
className: `${prefix}--file-filename`,
id: enhancedFileUploaderEnabled ? `${fileUploaderInstanceId}-file-${fileItems[file.index]?.uuid || file.index}` : `${fileUploaderInstanceId}-file-${file.index}`
}, file.name), /*#__PURE__*/React.createElement("span", {
className: `${prefix}--file__state-container`
}, /*#__PURE__*/React.createElement(Filename.default, {
name: file.name,
iconDescription: iconDescription,
status: filenameStatus,
onKeyDown: evt => {
if (match.matches(evt, [keys.Enter, keys.Space])) {
handleClick(evt, {
index: file.index,
filenameStatus
});
}
},
onClick: evt => handleClick(evt, {
index: file.index,
filenameStatus
})
}))))));
});
FileUploader.displayName = 'FileUploader';
FileUploader.propTypes = {
/**
* Specify the types of files that this input should be able to receive
*/
accept: PropTypes.arrayOf(PropTypes.string),
/**
* Specify the type of the `<FileUploaderButton>`
*/
buttonKind: PropTypes.oneOf(Button.ButtonKinds),
/**
* Provide the label text to be read by screen readers when interacting with
* the `<FileUploaderButton>`
*/
buttonLabel: PropTypes.string,
/**
* Provide a custom className to be applied to the container node
*/
className: PropTypes.string,
/**
* Specify whether file input is disabled
*/
disabled: PropTypes.bool,
/**
* Specify the status of the File Upload
*/
filenameStatus: PropTypes.oneOf(['edit', 'complete', 'uploading']).isRequired,
/**
* Provide a description for the complete/close icon that can be read by screen readers
*/
iconDescription: PropTypes.string,
/**
* Specify the description text of this `<FileUploader>`
*/
labelDescription: PropTypes.string,
/**
* Specify the title text of this `<FileUploader>`
*/
labelTitle: PropTypes.string,
/**
* Maximum file size allowed in bytes. Files larger than this will be marked invalid
*/
maxFileSize: PropTypes.number,
/**
* Specify if the component should accept multiple files to upload
*/
multiple: PropTypes.bool,
/**
* Provide a name for the underlying `<input>` node
*/
name: PropTypes.string,
/**
* Event handler that is called after files are added to the uploader
* The event handler signature looks like `onAddFiles(evt, { addedFiles })`
*/
onAddFiles: PropTypes.func,
/**
* Provide an optional `onChange` hook that is called each time the input is
* changed
*/
onChange: PropTypes.func,
/**
* Provide an optional `onClick` hook that is called each time the
* FileUploader is clicked
*/
onClick: PropTypes.func,
/**
* Provide an optional `onDelete` hook that is called when an uploaded item
* is removed
*/
onDelete: PropTypes.func,
/**
* Specify the size of the FileUploaderButton, from a list of available
* sizes.
*/
size: PropTypes.oneOf(['sm', 'small', 'md', 'field', 'lg'])
};
exports.default = FileUploader;