UNPKG

@adobe/coral-spectrum

Version:

Coral Spectrum is a JavaScript library of Web Components following Spectrum design patterns.

1,101 lines (912 loc) 31.2 kB
/** * Copyright 2019 Adobe. All rights reserved. * This file is licensed to you under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. You may obtain a copy * of the License at http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ import {BaseComponent} from '../../../coral-base-component'; import {BaseFormField} from '../../../coral-base-formfield'; import FileUploadItem from './FileUploadItem'; import base from '../templates/base'; import {transform, commons, validate} from '../../../coral-utils'; import {Decorator} from '../../../coral-decorator'; const CLASSNAME = '_coral-FileUpload'; const XHR_EVENT_NAMES = ['loadstart', 'progress', 'load', 'error', 'loadend', 'readystatechange', 'abort', 'timeout']; /** Enumeration for {@link FileUpload} HTTP methods that can be used to upload files. @typedef {Object} FileUploadMethodEnum @property {String} POST Send a POST request. Used when creating a resource. @property {String} PUT Send a PUT request. Used when replacing a resource. @property {String} PATCH Send a PATCH request. Used when partially updating a resource. */ const method = { POST: 'POST', PUT: 'PUT', PATCH: 'PATCH' }; /** @class Coral.FileUpload @classdesc A FileUpload component that manages the upload process of multiple files. Child elements of FileUpload can be given special attributes to enable functionality: - <code>[coral-fileupload-select]</code>. Click to choose file(s), replacing existing files. - <code>[coral-fileupload-dropzone]</code>. Drag and drop files to choose file(s), replacing existing files. - <code>[coral-fileupload-clear]</code>. Click to remove all files from the queue. - <code>[coral-fileupload-submit]</code>. Click to start uploading. - <code>[coral-fileupload-abort]</code>. Click to abort all uploads. - <code>[coral-fileupload-abortfile="filename.txt"]</code>. Click to abort a specific file, leaving it in the queue. - <code>[coral-fileupload-removefile="filename.txt"]</code>. Click to remove a specific file from the queue. - <code>[coral-fileupload-uploadfile="filename.txt"]</code>. Click to start uploading a specific file. @htmltag coral-fileupload @extends {HTMLElement} @extends {BaseComponent} @extends {BaseFormField} */ const FileUpload = Decorator(class extends BaseFormField(BaseComponent(HTMLElement)) { /** @ignore */ constructor() { super(); // Events this._delegateEvents(commons.extend(this._events, { // Clickable hooks 'click [coral-fileupload-submit]': '_onSubmitButtonClick', 'click [coral-fileupload-clear]': 'clear', 'click [coral-fileupload-select]': '_showFileDialog', 'click [coral-fileupload-abort]': 'abort', 'click [coral-fileupload-abortfile]': '_onAbortFileClick', 'click [coral-fileupload-removefile]': '_onRemoveFileClick', 'click [coral-fileupload-uploadfile]': '_onUploadFileClick', // Drag & Drop zones 'dragenter [coral-fileupload-dropzone]': '_onDragAndDrop', 'dragover [coral-fileupload-dropzone]': '_onDragAndDrop', 'dragleave [handle="input"]': '_onDragAndDrop', 'drop [handle="input"]': '_onDragAndDrop', // Accessibility 'capture:focus [coral-fileupload-select]': '_onButtonFocusIn', 'capture:focus [handle="input"]': '_onInputFocusIn', 'capture:blur [handle="input"]': '_onInputFocusOut' })); // Prepare templates this._elements = {}; base.call(this._elements, {commons}); // Pre-define labellable element this._labellableElement = this._elements.input; // Used for items this._uploadQueue = []; // this should refer to the fileupload this._doAddDragClass = this._doAddDragClass.bind(this); this._doRemoveDragClass = this._doRemoveDragClass.bind(this); this._positionInputOnDropZone = this._positionInputOnDropZone.bind(this); // Reposition the input under the specified dropzone this._observer = new MutationObserver(this._positionInputOnDropZone); this._observer.observe(this, { childList: true, attributes: true, attributeFilter: ['coral-fileupload-dropzone'], subtree: true }); } /** Name used to submit the data in a form. @type {String} @default "" @htmlattribute name @htmlattributereflected */ get name() { return this._elements.input.name; } set name(value) { this._reflectAttribute('name', value); this._elements.input.name = value; } /** This field's current value. @type {String} @default "" @htmlattribute value */ get value() { const item = this._uploadQueue ? this._getQueueItem(0) : null; // The first selected filename, or the empty string if no files are selected. return item ? `C:\\fakepath\\${item.file.name}` : ''; } set value(value) { if (value === '' || value === null) { this._clearQueue(); this._clearFileInputValue(); } else { // Throws exception if value is different than an empty string or null throw new Error('Coral.FileUpload accepts a filename, which may only be programmatically set to empty string.'); } } /** Whether this field is disabled or not. @type {Boolean} @default false @htmlattribute disabled @htmlattributereflected */ get disabled() { return this._elements.input.disabled; } set disabled(value) { this._elements.input.disabled = transform.booleanAttr(value); this._reflectAttribute('disabled', this.disabled); this.classList.toggle('is-disabled', this.disabled); this._setElementState(); } /** Inherited from {@link BaseFormField#invalid}. */ get invalid() { return super.invalid; } set invalid(value) { super.invalid = value; this._elements.input.setAttribute('aria-invalid', this.invalid); this._setElementState(); } /** Whether this field is required or not. @type {Boolean} @default false @htmlattribute required @htmlattributereflected */ get required() { return this._elements.input.required; } set required(value) { this._elements.input.required = transform.booleanAttr(value); this._reflectAttribute('required', this.required); this.classList.toggle('is-required', this.required); this._setElementState(); } /** Whether this field is readOnly or not. Indicating that the user cannot modify the value of the control. @type {Boolean} @default false @htmlattribute readonly @htmlattributereflected */ get readOnly() { return this._readOnly || false; } set readOnly(value) { this._readOnly = transform.booleanAttr(value); this._reflectAttribute('readonly', this._readOnly); this._setElementState(); } /** The names of the currently selected files. When {@link Coral.FileUpload#multiple} is <code>false</code>, this will be an array of length 1. @type {Array.<String>} */ get values() { let values = this._uploadQueue.map((item) => `C:\\fakepath\\${item.file.name}`); if (values.length && !this.multiple) { values = [values[0]]; } return values; } set values(values) { if (Array.isArray(values)) { if (values.length) { this.value = values[0]; } else { this.value = ''; } } } /** Inherited from {@link BaseFormField#labelledBy}. */ get labelledBy() { return super.labelledBy; } set labelledBy(value) { super.labelledBy = value; // The specified labelledBy property. const labelledBy = this.labelledBy; // An array of element ids to label control, the last being the select button element id. const ids = []; const button = this.querySelector('[coral-fileupload-select]'); if (button) { ids.push(button.id); } // If a labelledBy property exists, if (labelledBy) { // prepend the labelledBy value to the ids array ids.unshift(labelledBy); } // Set aria-labelledby attribute on the labellable element joining ids array into space-delimited list of ids. this._elements.input.setAttribute('aria-labelledby', ids.join(' ')); if (labelledBy) { // Set label for attribute const labelElement = document.getElementById(labelledBy); if (labelElement && labelElement.tagName === 'LABEL') { labelElement.setAttribute('for', this._elements.input.id); this._labelElement = labelElement; } } // Remove label for attribute else if (this._labelElement) { this._labelElement.removeAttribute('for'); } } /** Array of additional parameters as key:value to send in addition of files. A parameter must contain a <code>name</code> key:value and optionally a <code>value</code> key:value. @type {Array.<Object>} @default [] */ get parameters() { return this._parameters || []; } set parameters(values) { // Verify that every item has a name const isValid = Array.isArray(values) && values.every((el) => el && el.name); if (isValid) { this._parameters = values; if (!this.async) { Array.prototype.forEach.call(this.querySelectorAll('input[type="hidden"]'), (input) => { input.parentNode.removeChild(input); }); // Add extra parameters this.parameters.forEach((param) => { const input = document.createElement('input'); input.type = 'hidden'; input.name = param.name; input.value = param.value; this.appendChild(input); }); } } } /** Whether files should be uploaded asynchronously via XHR or synchronously e.g. within a <code>&lt;form&gt;</code> tag. One option excludes the other. Setting a new <code>async</code> value removes all files from the queue. @type {Boolean} @default false @htmlattribute async @htmlattributereflected */ get async() { return this._async || false; } set async(value) { this._async = transform.booleanAttr(value); this._reflectAttribute('async', this._async); // Sync extra parameters in case of form submission if (!this._async) { this.parameters = this.parameters; } // Clear file selection if (this._uploadQueue) { this._clearQueue(); this._clearFileInputValue(); } } /** The URL where the upload request should be sent. When used within a <code>&lt;form&gt;</code> tag to upload synchronously, the action of the form is used. If an element is clicked that has a <code>[coral-fileupload-submit]</code> attribute as well as a <code>[formaction]</code> attribute, the action of the clicked element will be used. Set this property before calling {@link Coral.FileUpload#upload} to reset the action set by a click. @type {String} @default "" @htmlattribute action @htmlattributereflected */ get action() { return this._action || ''; } set action(value) { this._action = transform.string(value); this._reflectAttribute('action', this._action); // Reset button action as action was set explicitly this._buttonAction = null; } /** The HTTP method to use when uploading files asynchronously. When used within a <code>&lt;form&gt;</code> tag to upload synchronously, the method of the form is used. If an element is clicked that has a <code>[coral-fileupload-submit]</code> attribute as well as a <code>[formmethod]</code> attribute, the method of the clicked element will be used. Set this property before calling {@link FileUpload#upload} to reset the method set by a click. See {@link FileUploadMethodEnum}. @type {String} @default FileUploadMethodEnum.POST @htmlattribute method @htmlattributereflected */ get method() { return this._method || method.POST; } set method(value) { value = transform.string(value).toUpperCase(); this._method = validate.enumeration(method)(value) && value || method.POST; this._reflectAttribute('method', this._method); // Reset button method as method was set explcitly this._buttonMethod = null; } /** Whether more than one file can be chosen at the same time to upload. @type {Boolean} @default false @htmlattribute multiple @htmlattributereflected */ get multiple() { return this._elements.input.multiple; } set multiple(value) { this._elements.input.multiple = transform.booleanAttr(value); this._reflectAttribute('multiple', this.multiple); } /** File size limit in bytes for one file. The value of 0 indicates unlimited, which is also the default. @type {Number} @htmlattribute sizelimit @htmlattributereflected @default 0 */ get sizeLimit() { return this._sizeLimit || 0; } set sizeLimit(value) { this._sizeLimit = transform.number(value); this._reflectAttribute('sizelimit', this._sizeLimit); } /** MIME types allowed for uploading (proper MIME types, wildcard '*' and file extensions are supported). To specify more than one value, separate the values with a comma (e.g. <code>&lt;input accept="audio/*,video/*,image/*" /&gt;</code>. @type {String} @default "" @htmlattribute accept @htmlattributereflected */ get accept() { return this._elements.input.accept; } set accept(value) { this._elements.input.accept = value; this._reflectAttribute('accept', this.accept); } /** Whether the upload should start immediately after file selection. @type {Boolean} @default false @htmlattribute autostart @htmlattributereflected */ get autoStart() { return this._autoStart || false; } set autoStart(value) { this._autoStart = transform.booleanAttr(value); this._reflectAttribute('autostart', this._autoStart); } /** Files to be uploaded. @readonly @default [] @type {Array.<Object>} */ get uploadQueue() { return this._uploadQueue; } /** @private */ _onButtonFocusIn(event) { // Get the input const input = this._elements.input; // Get the button const button = event.matchedTarget; // Move the input to after the button // This lets the next focused item be the correct one according to tab order button.parentNode.insertBefore(input, button.nextElementSibling); if (event.relatedTarget !== input) { // Make sure the input gets focused on FF window.setTimeout(() => { input.focus(); }, 100); } } /** @private */ _onInputFocusIn() { // Get the input const input = event.matchedTarget; const button = this.querySelector('[coral-fileupload-select]'); if (button) { // Remove from the tab order so shift+tab works button.tabIndex = -1; // So shifting focus backwards with screen reader doesn't create a focus trap button.setAttribute('aria-hidden', true); // Mark the button as focused button.classList.add('is-focused'); window.requestAnimationFrame(() => { if (input.classList.contains('focus-ring')) { button.classList.add('focus-ring'); } }); } } /** @private */ _onInputFocusOut() { // Unmark all the focused buttons const button = this.querySelector('[coral-fileupload-select].is-focused'); if (button) { button.classList.remove('is-focused'); button.classList.remove('focus-ring'); // Wait a frame so that shifting focus backwards with screen reader doesn't create a focus trap window.requestAnimationFrame(() => { button.tabIndex = 0; // @a11y: aria-hidden is removed to prevent focus trap when navigating backwards using a screen reader's // virtual cursor button.removeAttribute('aria-hidden'); }); } } /** @private */ _onAbortFileClick(event) { if (!this.async) { throw new Error('Coral.FileUpload does not support aborting file(s) upload on synchronous mode.'); } // Get file to abort const fileName = event.target.getAttribute('coral-fileupload-abortfile'); if (fileName) { this._abortFile(fileName); } } /** @private */ _onRemoveFileClick(event) { if (!this.async) { throw new Error('Coral.FileUpload does not support removing a file from the queue on synchronous mode.'); } else { // Get file to remove const fileName = event.target.getAttribute('coral-fileupload-removefile'); if (fileName) { this._clearFile(fileName); } } } /** @private */ _onUploadFileClick(event) { if (!this.async) { throw new Error('Coral.FileUpload does not support uploading a file from the queue on synchronous mode.'); } // Get file to upload const fileName = event.target.getAttribute('coral-fileupload-uploadfile'); if (fileName) { this.upload(fileName); } } /** @private */ _onDragAndDrop(event) { // Set dragging classes if (event.type === 'dragenter' || event.type === 'dragover') { this._addDragClass(); } else if (event.type === 'dragleave' || event.type === 'drop') { this._removeDragClass(); } this.trigger(`coral-fileupload:${event.type}`); } /** @private */ _addDragClass() { window.clearTimeout(this._removeClassTimeout); this._removeClassTimeout = window.setTimeout(this._doAddDragClass, 10); } /** @private */ _doAddDragClass() { this.classList.add('is-dragging'); const dropZone = this.querySelector('[coral-fileupload-dropzone]'); if (dropZone) { dropZone.classList.add('is-dragging'); } // Put the input on top to enable file drop this._elements.input.classList.remove('is-unselectable'); } /** @private */ _removeDragClass() { window.clearTimeout(this._removeClassTimeout); this._removeClassTimeout = window.setTimeout(this._doRemoveDragClass, 10); } /** @private */ _doRemoveDragClass() { this.classList.remove('is-dragging'); const dropZone = this.querySelector('[coral-fileupload-dropzone]'); if (dropZone) { dropZone.classList.remove('is-dragging'); } // Disable user interaction with the input this._elements.input.classList.add('is-unselectable'); } /** Handles clicks to submit buttons @private */ _onSubmitButtonClick(event) { const target = event.matchedTarget; // Override or reset the action/method given the button's configuration this._buttonAction = target.getAttribute('formaction'); // Make sure the method provided by the button is valid const buttonMethod = transform.string(target.getAttribute('formmethod')).toUpperCase(); this._buttonMethod = validate.enumeration(method)(buttonMethod) && buttonMethod || null; // Start the file upload this.upload(); } /** Handles changes to the input element. @private */ _onInputChange(event) { // Stop the current event event.stopPropagation(); if (this.disabled) { return; } let files = []; const items = []; // Retrieve files for select event if (event.target.files && event.target.files.length) { this._clearQueue(); files = event.target.files; // Verify if multiple file upload is allowed if (!this.multiple) { files = [files[0]]; } } // Retrieve files for drop event else if (event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files.length) { this._clearQueue(); files = event.dataTransfer.files; // Verify if multiple file upload is allowed if (!this.multiple) { files = [files[0]]; } } else { return; } // Initialize items for (let i = 0 ; i < files.length ; i++) { items.push(new FileUploadItem(files[i])); } // Verify if file is allowed to be uploaded and trigger events accordingly items.forEach((item) => { // If file is not found in uploadQueue using filename if (!this._getQueueItemByFilename(item.file.name)) { // Check file size if (this.sizeLimit && item.file.size > this.sizeLimit) { this.trigger('coral-fileupload:filesizeexceeded', {item}); } // Check mime type else if (this.accept && !item._isMimeTypeAllowed(this.accept)) { this.trigger('coral-fileupload:filemimetyperejected', {item}); } else { // Add item to queue this._uploadQueue.push(item); this.trigger('coral-fileupload:fileadded', {item}); } } }); if (this.autoStart) { this.upload(); } // Explicitly re-emit the change event if (this._triggerChangeEvent) { this.trigger('change'); } // Clear file input once files are added to the queue to make sure next file selection will trigger a change event if (this.async) { this._clearFileInputValue(); } } /** Sets the disabled/readonly state of elements with the associated special attributes @private */ _setElementState() { Array.prototype.forEach.call(this.querySelectorAll( '[coral-fileupload-select],' + '[coral-fileupload-dropzone],' + '[coral-fileupload-submit],' + '[coral-fileupload-clear],' + '[coral-fileupload-abort],' + '[coral-fileupload-abortfile],' + '[coral-fileupload-removefile],' + '[coral-fileupload-uploadfile]' ), (item) => { item.classList.toggle('is-invalid', this.invalid); item.classList.toggle('is-disabled', this.disabled); item.classList.toggle('is-required', this.required); item.classList.toggle('is-readOnly', this.readOnly); item[this.disabled || this.readOnly ? 'setAttribute' : 'removeAttribute']('disabled', ''); }); } /** @private */ _clearQueue() { this._uploadQueue.slice().forEach((item) => { this._clearFile(item.file.name); }); } /** Clear file selection on the file input @private */ _clearFileInputValue() { this._elements.input.value = ''; } /** Remove a file from the upload queue. @param {String} filename The filename of the file to remove. @private */ _clearFile(filename) { const item = this._getQueueItemByFilename(filename); if (item) { // Abort file upload this._abortFile(filename); // Remove file from queue this._uploadQueue.splice(this._getQueueIndex(filename), 1); this.trigger('coral-fileupload:fileremoved', {item}); } } /** Uploads a file in the queue. If an array is provided as the first argument, it is used as the parameters. @param filename {String} The name of the file to upload. @private */ _uploadFile(filename) { const item = this._getQueueItemByFilename(filename); if (item) { this._abortFile(filename); this._ajaxUpload(item); } } /** @private */ _showFileDialog() { // Show the dialog // This ONLY works when the call stack traces back to another click event! this._elements.input.click(); } /** Abort specific file upload. @param {String} filename The filename identifies the file to abort. @private */ _abortFile(filename) { const item = this._getQueueItemByFilename(filename); if (item && item._xhr) { item._xhr.abort(); item._xhr = null; } } /** Handles the ajax upload. @private */ _ajaxUpload(item) { // Use the action/method provided by the last button click, if provided const action = this._buttonAction || this.action; const requestMethod = this._buttonMethod ? this._buttonMethod.toUpperCase() : this.method; // We merge the global parameters with the specific file parameters and send them all together const parameters = this.parameters.concat(item.parameters); const formData = new FormData(); parameters.forEach((additionalParameter) => { formData.append(additionalParameter.name, additionalParameter.value); }); formData.append('_charset_', 'utf-8'); formData.append(this.name, item._originalFile); // Store the XHR on the item itself item._xhr = new XMLHttpRequest(); // Opening before being able to set response type to avoid IE11 InvalidStateError item._xhr.open(requestMethod, action); // Reflect specific xhr properties item._xhr.timeout = item.timeout; item._xhr.responseType = item.responseType; item._xhr.withCredentials = item.withCredentials; XHR_EVENT_NAMES.forEach((name) => { // Progress event is the only event among other ProgressEvents that can trigger multiple times. // Hence it's the only one that gives away usable progress information. const isProgressEvent = name === 'progress'; (isProgressEvent ? item._xhr.upload : item._xhr).addEventListener(name, (event) => { const detail = { item: item, action: action, method: requestMethod }; if (isProgressEvent) { detail.lengthComputable = event.lengthComputable; detail.loaded = event.loaded; detail.total = event.total; } this.trigger(`coral-fileupload:${name}`, detail); }); }); item._xhr.send(formData); } /** @private */ _getLabellableElement() { return this; } /** @private */ _getQueueItemByFilename(filename) { return this._getQueueItem(this._getQueueIndex(filename)); } /** @private */ _getQueueItem(index) { return index > -1 ? this._uploadQueue[index] : null; } /** @private */ _getQueueIndex(filename) { let index = -1; this._uploadQueue.some((item, i) => { if (item.file.name === filename) { index = i; return true; } return false; }); return index; } /** @private */ _getTargetChangeInput() { return this._elements.input; } /** @ignore */ _positionInputOnDropZone() { const input = this._elements.input; const dropZone = this.querySelector('[coral-fileupload-dropzone]'); if (dropZone) { const size = dropZone.getBoundingClientRect(); input.style.top = `${parseInt(dropZone.offsetTop, 10)}px`; input.style.left = `${parseInt(dropZone.offsetLeft, 10)}px`; input.style.width = `${parseInt(size.width, 10)}px`; input.style.height = `${parseInt(size.height, 10)}px`; } else { input.style.width = '0px'; input.style.height = '0px'; input.style.visibility = 'hidden'; } } /** Uploads the given filename, or all the files into the queue. It accepts extra parameters that are sent with the file. @param {String} [filename] The name of the file to upload. */ upload(filename) { if (!this.async) { if (typeof filename === 'string') { throw new Error('Coral.FileUpload does not support uploading a file from the queue on synchronous mode.'); } let form = this.closest('form'); if (!form) { form = document.createElement('form'); form.method = this.method.toLowerCase(); form.enctype = 'multipart/form-data'; form.action = this.action; form.hidden = true; form.appendChild(this._elements.input); Array.prototype.forEach.call(this.querySelectorAll('input[type="hidden"]'), (hiddenInput) => { form.appendChild(hiddenInput); }); // Make sure the form is connected before submission this.appendChild(form); } const input = document.createElement('input'); input.type = 'hidden'; input.name = '_charset_'; input.value = 'utf-8'; form.submit(); } else if (typeof filename === 'string') { this._uploadFile(filename); } else { this._uploadQueue.forEach((item) => { this._abortFile(item.file.name); this._ajaxUpload(item); }); } } /** Remove a file or all files from the upload queue. @param {String} [filename] The filename of the file to remove. If a filename is not provided, all files will be removed. */ clear(filename) { if (!this.async) { if (typeof filename === 'string') { throw new Error('Coral.FileUpload does not support removing a file from the queue on synchronous mode.'); } this._clearQueue(); this._clearFileInputValue(); } else if (typeof filename === 'string') { this._clearFile(filename); } else { this._clearQueue(); } } /** Abort upload of a given file or all files in the queue. @param {String} [filename] The filename of the file to abort. If a filename is not provided, all files will be aborted. */ abort(filename) { if (!this.async) { throw new Error('Coral.FileUpload does not support aborting file(s) upload on synchronous mode.'); } if (typeof filename === 'string') { // Abort a single file this._abortFile(filename); } else { // Abort all files this._uploadQueue.forEach((item) => { this._abortFile(item.file.name); }); } } static get _attributePropertyMap() { return commons.extend(super._attributePropertyMap, { sizelimit: 'sizeLimit', autostart: 'autoStart' }); } /** @ignore */ static get observedAttributes() { return super.observedAttributes.concat([ 'async', 'action', 'method', 'multiple', 'sizelimit', 'accept', 'autostart' ]); } /** @ignore */ render() { super.render(); this.classList.add(CLASSNAME); const button = this.querySelector('[coral-fileupload-select]'); if (button) { button.id = button.id || commons.getUID(); } // If no labelledby is specified, ensure input is at labelledby the select button this.labelledBy = this.labelledBy; // Fetch additional parameters if any const parameters = []; Array.prototype.forEach.call(this.querySelectorAll('input[type="hidden"]'), (input) => { parameters.push({ name: input.name, value: input.value }); }); this.parameters = parameters; // Remove the input if it's already there // A fresh input is preferred to value = '' as it may not work in all browsers const inputElement = this.querySelector('[handle="input"]'); if (inputElement) { inputElement.parentNode.removeChild(inputElement); } // Add the input to the component this.appendChild(this._elements.input); // IE11 requires one more frame or the resize listener <object> will appear as an overlaying white box window.requestAnimationFrame(() => { // Handles the repositioning of the input to allow dropping files commons.addResizeListener(this, this._positionInputOnDropZone); }); } }); export default FileUpload;