From 1ee697e066a9e2fbd6867f6ceeae305f4e4dca27 Mon Sep 17 00:00:00 2001 From: Bartosz Kepka Date: Thu, 2 Jul 2026 10:47:27 +0200 Subject: [PATCH 1/4] feat(content-uploader): add drop support for modernized uploads manager --- .../content-uploader/ContentUploader.tsx | 20 ++++- .../ModernizedUploadsManagerDropZone.tsx | 62 +++++++++++++ .../__tests__/ContentUploader.test.js | 89 +++++++++++++++++++ 3 files changed, 167 insertions(+), 4 deletions(-) create mode 100644 src/elements/content-uploader/ModernizedUploadsManagerDropZone.tsx diff --git a/src/elements/content-uploader/ContentUploader.tsx b/src/elements/content-uploader/ContentUploader.tsx index 89dc29a610..74bd6b4ff9 100644 --- a/src/elements/content-uploader/ContentUploader.tsx +++ b/src/elements/content-uploader/ContentUploader.tsx @@ -13,6 +13,7 @@ import CancelAllUploadsModal from './CancelAllUploadsModal'; import LargeFileWarningModal from './LargeFileWarningModal'; import DroppableContent from './DroppableContent'; import Footer from './Footer'; +import ModernizedUploadsManagerDropZone from './ModernizedUploadsManagerDropZone'; import UploadsManager from './UploadsManager'; import { getUploadItemKey, mapToModernizedUploadItems } from './utils/mapToModernizedUploadItem'; import './ModernizedUploadsManagerPanel.scss'; @@ -76,6 +77,7 @@ export interface ContentUploaderProps { dataTransferItems: Array; fileLimit: number; files?: Array; + canDropOnUploadsManager?: boolean; isDraggingItemsToUploadsManager?: boolean; isFolderUploadEnabled: boolean; isLarge: boolean; @@ -172,6 +174,7 @@ class ContentUploader extends Component { clientName: CLIENT_NAME_CONTENT_UPLOADER, dataTransferItems: [], files: [], + canDropOnUploadsManager: true, fileLimit: FILE_LIMIT_DEFAULT, isDraggingItemsToUploadsManager: false, isFolderUploadEnabled: false, @@ -652,7 +655,8 @@ class ContentUploader extends Component { files: Array, itemUpdateCallback: Function, ) => { - const { rootFolderId } = this.props; + const { enableModernizedUploads, rootFolderId, useUploadsManager } = this.props; + const shouldAddRootFolderIdToUploadOptions = enableModernizedUploads && useUploadsManager; // Convert files from the file API to upload items const newItems = files.map(file => { @@ -678,7 +682,9 @@ class ContentUploader extends Component { }; if (uploadAPIOptions) { - uploadItem.options = uploadAPIOptions; + uploadItem.options = shouldAddRootFolderIdToUploadOptions + ? { folderId: rootFolderId, ...uploadAPIOptions } + : uploadAPIOptions; } this.itemIdsRef.current[getFileId(uploadItem, rootFolderId)] = true; @@ -1741,6 +1747,7 @@ class ContentUploader extends Component { render() { const { className, + canDropOnUploadsManager, enableModernizedUploads, fileLimit, isDraggingItemsToUploadsManager = false, @@ -1795,7 +1802,12 @@ class ContentUploader extends Component {
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */} -
+ this.addDroppedItemsToUploadQueue(droppedItems, this.upload) + } + allowedTypes={['Files']} + canDrop={canDropOnUploadsManager} className={classNames('bcu-modernized-panel', { visible: modernizedPanelState === 'shown', dismissing: modernizedPanelState === 'dismissing', @@ -1833,7 +1845,7 @@ class ContentUploader extends Component { onUpgradeCTAClick={onUpgradeCTAClick} oversizeFiles={oversizeFiles} /> -
+
); } diff --git a/src/elements/content-uploader/ModernizedUploadsManagerDropZone.tsx b/src/elements/content-uploader/ModernizedUploadsManagerDropZone.tsx new file mode 100644 index 0000000000..646e8fc7ac --- /dev/null +++ b/src/elements/content-uploader/ModernizedUploadsManagerDropZone.tsx @@ -0,0 +1,62 @@ +import * as React from 'react'; + +import makeDroppable from '../common/droppable'; +import type { DOMStringList } from '../../common/types/core'; + +export interface ModernizedUploadsManagerDropZoneProps extends React.HTMLAttributes { + addDataTransferItemsToUploadQueue: (droppedItems: DataTransfer) => void; + allowedTypes: Array; + canDrop?: boolean; + children: React.ReactNode; + className: string; +} + +const dropDefinition = { + dropValidator: ( + { allowedTypes, canDrop = true }: ModernizedUploadsManagerDropZoneProps, + { types }: { types: Array | DOMStringList }, + ) => { + if (!canDrop) { + return false; + } + + if (types instanceof Array) { + return Array.from(types).some(type => allowedTypes.indexOf(type) > -1); + } + + const allowedList = allowedTypes.filter(allowed => types.contains(allowed)); + return allowedList.length > 0; + }, + + onDrop: (event: DragEvent, { addDataTransferItemsToUploadQueue }: ModernizedUploadsManagerDropZoneProps) => { + const { dataTransfer } = event; + + if (!dataTransfer) { + return; + } + + addDataTransferItemsToUploadQueue(dataTransfer); + }, +} as const; + +const ModernizedUploadsManagerDropZoneComponent = React.forwardRef< + HTMLDivElement, + ModernizedUploadsManagerDropZoneProps +>((props, ref) => { + const { children } = props; + const htmlProps: Partial = { ...props }; + delete htmlProps.addDataTransferItemsToUploadQueue; + delete htmlProps.allowedTypes; + delete htmlProps.canDrop; + delete htmlProps.children; + + return ( +
+ {children} +
+ ); +}); + +const ModernizedUploadsManagerDropZone = makeDroppable(dropDefinition)(ModernizedUploadsManagerDropZoneComponent); + +export default ModernizedUploadsManagerDropZone; diff --git a/src/elements/content-uploader/__tests__/ContentUploader.test.js b/src/elements/content-uploader/__tests__/ContentUploader.test.js index 82e1e22bfc..e5fc874c15 100644 --- a/src/elements/content-uploader/__tests__/ContentUploader.test.js +++ b/src/elements/content-uploader/__tests__/ContentUploader.test.js @@ -7,6 +7,7 @@ import { ContentUploaderComponent, CHUNKED_UPLOAD_MIN_SIZE_BYTES } from '../Cont import Footer from '../Footer'; import UploadsManager from '../UploadsManager'; import DroppableContent from '../DroppableContent'; +import ModernizedUploadsManagerDropZone from '../ModernizedUploadsManagerDropZone'; import { ERROR_CODE_ITEM_NAME_IN_USE, STATUS_PENDING, @@ -159,6 +160,26 @@ describe('elements/content-uploader/ContentUploader', () => { expect(instance.itemIdsRef.current).toEqual(expected); }); + test('should add rootFolderId to raw file upload options for the modernized uploads manager', () => { + const wrapper = getWrapper({ + enableModernizedUploads: true, + rootFolderId: '12345', + useUploadsManager: true, + }); + + wrapper.instance().addFilesToUploadQueue([{ name: 'yoyo', size: 1000 }], jest.fn(), false); + + expect(wrapper.state().items[0].options.folderId).toBe('12345'); + }); + + test('should not add rootFolderId to raw file upload options outside the modernized uploads manager', () => { + const wrapper = getWrapper({ rootFolderId: '12345' }); + + wrapper.instance().addFilesToUploadQueue([{ name: 'yoyo', size: 1000 }], jest.fn(), false); + + expect(wrapper.state().items[0].options.folderId).toBeUndefined(); + }); + test('should handle accepting package "files" separate from folders', () => { const mockFile = { name: 'hi' }; Browser.isSafari = jest.fn(() => true); @@ -964,6 +985,73 @@ describe('elements/content-uploader/ContentUploader', () => { }); }); + describe('ModernizedUploadsManagerDropZone', () => { + test('prevents browser navigation and queues files dropped on the modernized panel', () => { + const addDataTransferItemsToUploadQueue = jest.fn(); + const dataTransfer = { + items: ['file-item'], + types: ['Files'], + }; + const dragEnterEvent = { + dataTransfer, + preventDefault: jest.fn(), + }; + const dropEvent = { + dataTransfer, + preventDefault: jest.fn(), + }; + const wrapper = shallow( + +
+ , + ); + + wrapper.instance().handleDragEnter(dragEnterEvent); + wrapper.instance().handleDrop(dropEvent); + + expect(dragEnterEvent.preventDefault).toHaveBeenCalled(); + expect(dropEvent.preventDefault).toHaveBeenCalled(); + expect(addDataTransferItemsToUploadQueue).toHaveBeenCalledWith(dataTransfer); + }); + + test('prevents browser navigation without queueing when drop is disabled', () => { + const addDataTransferItemsToUploadQueue = jest.fn(); + const dataTransfer = { + items: ['file-item'], + types: ['Files'], + }; + const dragEnterEvent = { + dataTransfer, + preventDefault: jest.fn(), + }; + const dropEvent = { + dataTransfer, + preventDefault: jest.fn(), + }; + const wrapper = shallow( + +
+ , + ); + + wrapper.instance().handleDragEnter(dragEnterEvent); + wrapper.instance().handleDrop(dropEvent); + + expect(dragEnterEvent.preventDefault).toHaveBeenCalled(); + expect(dropEvent.preventDefault).toHaveBeenCalled(); + expect(addDataTransferItemsToUploadQueue).not.toHaveBeenCalled(); + }); + }); + describe('render()', () => { describe('enableModernizedUploads', () => { test('should render legacy UploadsManager when enableModernizedUploads is false and useUploadsManager is true', () => { @@ -985,6 +1073,7 @@ describe('elements/content-uploader/ContentUploader', () => { expect(wrapper.find(UploadsManagerBP)).toHaveLength(1); expect(wrapper.find(UploadsManager)).toHaveLength(0); expect(wrapper.find(DroppableContent)).toHaveLength(0); + expect(wrapper.find(ModernizedUploadsManagerDropZone)).toHaveLength(1); }); test('should render modernized UploadsManagerBP even when useUploadsManager is true', () => { From 7df0d45e89e65df6a794ef1f25373b98684d66d0 Mon Sep 17 00:00:00 2001 From: Bartosz Kepka Date: Thu, 2 Jul 2026 13:04:53 +0200 Subject: [PATCH 2/4] fix: drop zone DOM prop leakage and disabled-drop gate --- .../content-uploader/ContentUploader.tsx | 3 +- .../ModernizedUploadsManagerDropZone.tsx | 29 +++++++--- .../__tests__/ContentUploader.test.js | 58 ++++++++++++++++++- 3 files changed, 77 insertions(+), 13 deletions(-) diff --git a/src/elements/content-uploader/ContentUploader.tsx b/src/elements/content-uploader/ContentUploader.tsx index 74bd6b4ff9..1a555cbc7b 100644 --- a/src/elements/content-uploader/ContentUploader.tsx +++ b/src/elements/content-uploader/ContentUploader.tsx @@ -1801,13 +1801,12 @@ class ContentUploader extends Component { return (
- {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */} this.addDroppedItemsToUploadQueue(droppedItems, this.upload) } allowedTypes={['Files']} - canDrop={canDropOnUploadsManager} + isDropEnabled={canDropOnUploadsManager} className={classNames('bcu-modernized-panel', { visible: modernizedPanelState === 'shown', dismissing: modernizedPanelState === 'dismissing', diff --git a/src/elements/content-uploader/ModernizedUploadsManagerDropZone.tsx b/src/elements/content-uploader/ModernizedUploadsManagerDropZone.tsx index 646e8fc7ac..eea66c5f2d 100644 --- a/src/elements/content-uploader/ModernizedUploadsManagerDropZone.tsx +++ b/src/elements/content-uploader/ModernizedUploadsManagerDropZone.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import omit from 'lodash/omit'; import makeDroppable from '../common/droppable'; import type { DOMStringList } from '../../common/types/core'; @@ -6,17 +7,23 @@ import type { DOMStringList } from '../../common/types/core'; export interface ModernizedUploadsManagerDropZoneProps extends React.HTMLAttributes { addDataTransferItemsToUploadQueue: (droppedItems: DataTransfer) => void; allowedTypes: Array; - canDrop?: boolean; children: React.ReactNode; className: string; + isDropEnabled?: boolean; +} + +interface DroppableStateProps { + canDrop?: boolean; + isDragging?: boolean; + isOver?: boolean; } const dropDefinition = { dropValidator: ( - { allowedTypes, canDrop = true }: ModernizedUploadsManagerDropZoneProps, + { allowedTypes, isDropEnabled = true }: ModernizedUploadsManagerDropZoneProps, { types }: { types: Array | DOMStringList }, ) => { - if (!canDrop) { + if (!isDropEnabled) { return false; } @@ -41,14 +48,18 @@ const dropDefinition = { const ModernizedUploadsManagerDropZoneComponent = React.forwardRef< HTMLDivElement, - ModernizedUploadsManagerDropZoneProps + ModernizedUploadsManagerDropZoneProps & DroppableStateProps >((props, ref) => { const { children } = props; - const htmlProps: Partial = { ...props }; - delete htmlProps.addDataTransferItemsToUploadQueue; - delete htmlProps.allowedTypes; - delete htmlProps.canDrop; - delete htmlProps.children; + const htmlProps: React.HTMLAttributes = omit(props, [ + 'addDataTransferItemsToUploadQueue', + 'allowedTypes', + 'canDrop', + 'children', + 'isDragging', + 'isDropEnabled', + 'isOver', + ]); return (
diff --git a/src/elements/content-uploader/__tests__/ContentUploader.test.js b/src/elements/content-uploader/__tests__/ContentUploader.test.js index e5fc874c15..797ffb29b8 100644 --- a/src/elements/content-uploader/__tests__/ContentUploader.test.js +++ b/src/elements/content-uploader/__tests__/ContentUploader.test.js @@ -3,6 +3,7 @@ import { shallow } from 'enzyme'; import { UploadsManager as UploadsManagerBP } from '@box/uploads-manager'; import * as UploaderUtils from '../../../utils/uploads'; import Browser from '../../../utils/Browser'; +import { fireEvent, render, screen } from '../../../test-utils/testing-library'; import { ContentUploaderComponent, CHUNKED_UPLOAD_MIN_SIZE_BYTES } from '../ContentUploader'; import Footer from '../Footer'; import UploadsManager from '../UploadsManager'; @@ -986,6 +987,19 @@ describe('elements/content-uploader/ContentUploader', () => { }); describe('ModernizedUploadsManagerDropZone', () => { + const renderModernizedUploadsManagerDropZone = props => + render( + +
+ , + ); + test('prevents browser navigation and queues files dropped on the modernized panel', () => { const addDataTransferItemsToUploadQueue = jest.fn(); const dataTransfer = { @@ -1018,7 +1032,7 @@ describe('elements/content-uploader/ContentUploader', () => { expect(addDataTransferItemsToUploadQueue).toHaveBeenCalledWith(dataTransfer); }); - test('prevents browser navigation without queueing when drop is disabled', () => { + test('prevents browser navigation without queueing when isDropEnabled is false', () => { const addDataTransferItemsToUploadQueue = jest.fn(); const dataTransfer = { items: ['file-item'], @@ -1036,7 +1050,7 @@ describe('elements/content-uploader/ContentUploader', () => {
@@ -1050,6 +1064,46 @@ describe('elements/content-uploader/ContentUploader', () => { expect(dropEvent.preventDefault).toHaveBeenCalled(); expect(addDataTransferItemsToUploadQueue).not.toHaveBeenCalled(); }); + + test('does not forward droppable state props to the modernized panel DOM element', () => { + const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {}); + const dataTransfer = { + items: ['file-item'], + types: ['Files'], + }; + let consoleErrors = ''; + + try { + renderModernizedUploadsManagerDropZone(); + + fireEvent.dragEnter(screen.getByTestId('bcu-modernized-panel'), { dataTransfer }); + + consoleErrors = consoleError.mock.calls.flat().join('\n'); + } finally { + consoleError.mockRestore(); + } + + expect(consoleErrors).not.toContain('isDragging'); + expect(consoleErrors).not.toContain('isOver'); + }); + + test('does not queue files dropped on the modernized panel when isDropEnabled is false', () => { + const addDataTransferItemsToUploadQueue = jest.fn(); + const dataTransfer = { + items: ['file-item'], + types: ['Files'], + }; + + renderModernizedUploadsManagerDropZone({ + addDataTransferItemsToUploadQueue, + isDropEnabled: false, + }); + + fireEvent.dragEnter(screen.getByTestId('bcu-modernized-panel'), { dataTransfer }); + fireEvent.drop(screen.getByTestId('bcu-modernized-panel'), { dataTransfer }); + + expect(addDataTransferItemsToUploadQueue).not.toHaveBeenCalled(); + }); }); describe('render()', () => { From f8fb79cb83cf7a003bcf7b5d3090ff967a4b3adc Mon Sep 17 00:00:00 2001 From: Bartosz Kepka Date: Thu, 2 Jul 2026 13:58:19 +0200 Subject: [PATCH 3/4] fix: change default prop --- src/elements/content-uploader/ContentUploader.tsx | 2 +- .../content-uploader/__tests__/ContentUploader.test.js | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/elements/content-uploader/ContentUploader.tsx b/src/elements/content-uploader/ContentUploader.tsx index 1a555cbc7b..714af473b2 100644 --- a/src/elements/content-uploader/ContentUploader.tsx +++ b/src/elements/content-uploader/ContentUploader.tsx @@ -174,7 +174,7 @@ class ContentUploader extends Component { clientName: CLIENT_NAME_CONTENT_UPLOADER, dataTransferItems: [], files: [], - canDropOnUploadsManager: true, + canDropOnUploadsManager: false, fileLimit: FILE_LIMIT_DEFAULT, isDraggingItemsToUploadsManager: false, isFolderUploadEnabled: false, diff --git a/src/elements/content-uploader/__tests__/ContentUploader.test.js b/src/elements/content-uploader/__tests__/ContentUploader.test.js index 797ffb29b8..082bf3292f 100644 --- a/src/elements/content-uploader/__tests__/ContentUploader.test.js +++ b/src/elements/content-uploader/__tests__/ContentUploader.test.js @@ -1130,6 +1130,12 @@ describe('elements/content-uploader/ContentUploader', () => { expect(wrapper.find(ModernizedUploadsManagerDropZone)).toHaveLength(1); }); + test('should disable dropping on the modernized panel when modernized drop props are omitted', () => { + const wrapper = getWrapper({ enableModernizedUploads: true }); + + expect(wrapper.find(ModernizedUploadsManagerDropZone).prop('isDropEnabled')).toBe(false); + }); + test('should render modernized UploadsManagerBP even when useUploadsManager is true', () => { const wrapper = getWrapper({ enableModernizedUploads: true, useUploadsManager: true }); expect(wrapper.find(UploadsManagerBP)).toHaveLength(1); From 7245efa62d9ee59121a13e3073d99bd9a0c9985d Mon Sep 17 00:00:00 2001 From: Bartosz Kepka Date: Fri, 3 Jul 2026 10:07:30 +0200 Subject: [PATCH 4/4] fix: address review feedback --- .../__tests__/ContentUploader.test.js | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/elements/content-uploader/__tests__/ContentUploader.test.js b/src/elements/content-uploader/__tests__/ContentUploader.test.js index 082bf3292f..354669af96 100644 --- a/src/elements/content-uploader/__tests__/ContentUploader.test.js +++ b/src/elements/content-uploader/__tests__/ContentUploader.test.js @@ -162,21 +162,28 @@ describe('elements/content-uploader/ContentUploader', () => { }); test('should add rootFolderId to raw file upload options for the modernized uploads manager', () => { + const files = [{ name: 'yoyo', size: 1000 }]; const wrapper = getWrapper({ enableModernizedUploads: true, rootFolderId: '12345', useUploadsManager: true, }); + wrapper.instance().upload = jest.fn(); - wrapper.instance().addFilesToUploadQueue([{ name: 'yoyo', size: 1000 }], jest.fn(), false); + wrapper.setProps({ files }); expect(wrapper.state().items[0].options.folderId).toBe('12345'); }); test('should not add rootFolderId to raw file upload options outside the modernized uploads manager', () => { - const wrapper = getWrapper({ rootFolderId: '12345' }); + const files = [{ name: 'yoyo', size: 1000 }]; + const wrapper = getWrapper({ + rootFolderId: '12345', + useUploadsManager: true, + }); + wrapper.instance().upload = jest.fn(); - wrapper.instance().addFilesToUploadQueue([{ name: 'yoyo', size: 1000 }], jest.fn(), false); + wrapper.setProps({ files }); expect(wrapper.state().items[0].options.folderId).toBeUndefined(); }); @@ -1099,8 +1106,9 @@ describe('elements/content-uploader/ContentUploader', () => { isDropEnabled: false, }); - fireEvent.dragEnter(screen.getByTestId('bcu-modernized-panel'), { dataTransfer }); - fireEvent.drop(screen.getByTestId('bcu-modernized-panel'), { dataTransfer }); + const modernizedPanel = screen.getByTestId('bcu-modernized-panel'); + fireEvent.dragEnter(modernizedPanel, { dataTransfer }); + fireEvent.drop(modernizedPanel, { dataTransfer }); expect(addDataTransferItemsToUploadQueue).not.toHaveBeenCalled(); });