UNPKG

@carbon/react

Version:

React components for the Carbon Design System

283 lines (281 loc) 10.2 kB
/** * 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;