@carbon/react
Version:
React components for the Carbon Design System
283 lines (281 loc) • 10.2 kB
JavaScript
/**
* Copyright IBM Corp. 2016, 2026
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/
const require_runtime = require("../../_virtual/_rolldown/runtime.js");
const require_usePrefix = require("../../internal/usePrefix.js");
const require_Text = require("../Text/Text.js");
const require_keys = require("../../internal/keyboard/keys.js");
const require_match = require("../../internal/keyboard/match.js");
const require_useId = require("../../internal/useId.js");
const require_index = require("../FeatureFlags/index.js");
const require_Button = require("../Button/Button.js");
const require_Filename = require("./Filename.js");
const require_FileUploaderButton = require("./FileUploaderButton.js");
let classnames = require("classnames");
classnames = require_runtime.__toESM(classnames);
let react = require("react");
react = require_runtime.__toESM(react);
let prop_types = require("prop-types");
prop_types = require_runtime.__toESM(prop_types);
let react_jsx_runtime = require("react/jsx-runtime");
//#region src/components/FileUploader/FileUploader.tsx
/**
* Copyright IBM Corp. 2016, 2026
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/
const FileUploader = (0, react.forwardRef)(({ accept, buttonKind, buttonLabel, className, disabled, filenameStatus, iconDescription, labelDescription, labelTitle, maxFileSize, multiple, name, onAddFiles, onChange, onClick, onDelete, size, ...other }, ref) => {
const fileUploaderInstanceId = require_useId.useId("file-uploader");
const prefix = require_usePrefix.usePrefix();
const enhancedFileUploaderEnabled = require_index.useFeatureFlag("enable-enhanced-file-uploader");
const [fileItems, setFileItems] = (0, react.useState)([]);
const [legacyFileNames, setLegacyFileNames] = (0, react.useState)([]);
const uploaderButton = react.default.createRef();
const nodes = [];
const createFileItem = (0, 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 = (0, react.useCallback)((files) => {
return files.map((file) => {
if (maxFileSize && file.size > maxFileSize) file.invalidFileType = true;
return file;
});
}, [maxFileSize]);
const handleChange = (0, react.useCallback)((evt) => {
evt.stopPropagation();
const incomingFiles = Array.from(evt.target.files);
const validatedFiles = validateFiles(multiple ? incomingFiles : [incomingFiles[0]]);
if (onAddFiles) onAddFiles(evt, { addedFiles: validatedFiles });
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);
onChange({
...evt,
target: {
...evt.target,
files: Object.assign(allFiles, { item: (index) => allFiles[index] || null }),
addedFiles: newFileItems,
currentFiles: updatedFileItems,
action: "add"
}
});
}
} else {
const filenames = validFiles.map((file) => file.name);
setLegacyFileNames(multiple ? [...new Set([...legacyFileNames, ...filenames])] : filenames);
if (onChange) onChange(evt);
}
}, [
enhancedFileUploaderEnabled,
fileItems,
legacyFileNames,
multiple,
onAddFiles,
onChange,
createFileItem,
validateFiles
]);
const handleClick = (0, 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];
setLegacyFileNames(legacyFileNames.filter((filename) => filename !== deletedFileName));
if (onDelete) onDelete(evt);
}
if (onClick) onClick(evt);
uploaderButton.current?.focus?.();
}
}, [
enhancedFileUploaderEnabled,
fileItems,
legacyFileNames,
onDelete,
onChange,
onClick,
uploaderButton
]);
(0, react.useImperativeHandle)(ref, () => ({
clearFiles() {
if (enhancedFileUploaderEnabled) {
const previousItems = [...fileItems];
setFileItems([]);
if (onChange && previousItems.length > 0) onChange({
target: {
files: Object.assign([], { item: () => null }),
clearedFiles: previousItems,
currentFiles: [],
action: "clear"
},
preventDefault: () => {},
stopPropagation: () => {}
});
} else setLegacyFileNames([]);
},
...enhancedFileUploaderEnabled && { getCurrentFiles() {
return [...fileItems];
} }
}), [
enhancedFileUploaderEnabled,
fileItems,
onChange
]);
const classes = (0, classnames.default)({
[`${prefix}--form-item`]: true,
[className]: className
});
const getHelperLabelClasses = (baseClass) => (0, classnames.default)(baseClass, { [`${prefix}--label-description--disabled`]: disabled });
const selectedFileClasses = (0, classnames.default)(`${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__ */ (0, react_jsx_runtime.jsxs)("div", {
className: classes,
...other,
children: [
!labelTitle ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_Text.Text, {
as: "h3",
className: getHelperLabelClasses(`${prefix}--file--label`),
children: labelTitle
}),
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_Text.Text, {
as: "p",
className: getHelperLabelClasses(`${prefix}--label-description`),
id: fileUploaderInstanceId,
children: labelDescription
}),
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_FileUploaderButton.default, {
innerRef: uploaderButton,
disabled,
labelText: buttonLabel,
multiple,
buttonKind,
onChange: handleChange,
disableLabelChanges: true,
accept,
name,
size,
"aria-describedby": fileUploaderInstanceId
}),
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
className: `${prefix}--file-container`,
children: displayFiles.length === 0 ? null : displayFiles.map((file) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
className: selectedFileClasses,
ref: (node) => {
nodes[file.index] = node;
},
...other,
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_Text.Text, {
as: "p",
className: `${prefix}--file-filename`,
id: enhancedFileUploaderEnabled ? `${fileUploaderInstanceId}-file-${fileItems[file.index]?.uuid || file.index}` : `${fileUploaderInstanceId}-file-${file.index}`,
children: file.name
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
className: `${prefix}--file__state-container`,
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_Filename.default, {
name: file.name,
iconDescription,
status: filenameStatus,
onKeyDown: (evt) => {
if (require_match.matches(evt, [require_keys.Enter, require_keys.Space])) handleClick(evt, {
index: file.index,
filenameStatus
});
},
onClick: (evt) => handleClick(evt, {
index: file.index,
filenameStatus
})
})
})]
}, file.key))
})
]
});
});
FileUploader.displayName = "FileUploader";
FileUploader.propTypes = {
accept: prop_types.default.arrayOf(prop_types.default.string),
buttonKind: prop_types.default.oneOf(require_Button.ButtonKinds),
buttonLabel: prop_types.default.string,
className: prop_types.default.string,
disabled: prop_types.default.bool,
filenameStatus: prop_types.default.oneOf([
"edit",
"complete",
"uploading"
]).isRequired,
iconDescription: prop_types.default.string,
labelDescription: prop_types.default.string,
labelTitle: prop_types.default.string,
maxFileSize: prop_types.default.number,
multiple: prop_types.default.bool,
name: prop_types.default.string,
onAddFiles: prop_types.default.func,
onChange: prop_types.default.func,
onClick: prop_types.default.func,
onDelete: prop_types.default.func,
size: prop_types.default.oneOf([
"sm",
"small",
"md",
"field",
"lg"
])
};
//#endregion
exports.default = FileUploader;