diff --git a/demo/app/components/file-upload/file-upload.component.html b/demo/app/components/file-upload/file-upload.component.html index f1188e1cb..5d4ea395e 100644 --- a/demo/app/components/file-upload/file-upload.component.html +++ b/demo/app/components/file-upload/file-upload.component.html @@ -6,12 +6,14 @@

Demo Controls

Allow multiple selections
Hide button +
+ Disabled
-
+
Demo Controls @@ -85,7 +87,7 @@ class="c-file-upload__prompt qa-file-upload-prompt" [class.c-file-upload__prompt--hidden]="hideButton" [theme]="theme" - [isDisabled]="dragInProgress" + [isDisabled]="dragInProgress || isDisabled" (clicked)="promptForFiles()" > {{ buttonMessage }} diff --git a/terminus-ui/file-upload/src/file-upload.component.scss b/terminus-ui/file-upload/src/file-upload.component.scss index ec315d5fa..6de8d9279 100644 --- a/terminus-ui/file-upload/src/file-upload.component.scss +++ b/terminus-ui/file-upload/src/file-upload.component.scss @@ -21,7 +21,9 @@ $drag-bg: lighten(color(primary, xlight), 50%); // Top level styles belong here .c-file-upload { - cursor: cursor(pointer); + &:not(.c-file-upload--disabled) { + cursor: cursor(pointer); + } } // If followed directly by another instance, add vertical spacing @@ -39,7 +41,9 @@ $drag-bg: lighten(color(primary, xlight), 50%); &:hover, &:focus { - border-color: color(primary, xlight); + &:not(.c-file-upload--disabled) { + border-color: color(primary, xlight); + } } // Class added when dragging over @@ -63,6 +67,12 @@ $drag-bg: lighten(color(primary, xlight), 50%); border-color: color(warn); } + &.c-file-upload--disabled { + .c-file-upload__empty { + color: color(utility, light); + } + } + //
container for the selected file .c-file-upload__file { @include typography(hint); diff --git a/terminus-ui/file-upload/src/file-upload.component.spec.ts b/terminus-ui/file-upload/src/file-upload.component.spec.ts index 0f40d4b7a..4e6bf501a 100644 --- a/terminus-ui/file-upload/src/file-upload.component.spec.ts +++ b/terminus-ui/file-upload/src/file-upload.component.spec.ts @@ -7,7 +7,9 @@ import { TestBed, TestModuleMetadata, } from '@angular/core/testing'; +import { FormControl } from '@angular/forms'; import { By } from '@angular/platform-browser'; +import { KEYS } from '@terminus/ngx-tools/keycodes'; import { configureTestBedWithoutReset, createFakeEvent, @@ -17,13 +19,12 @@ import { } from '@terminus/ngx-tools/testing'; import { TsStyleThemeTypes } from '@terminus/ui/utilities'; -import { FormControl } from '@angular/forms'; -import { KEYS } from '@terminus/ngx-tools/keycodes'; import { TsFileUploadComponent } from './file-upload.component'; import { TsFileUploadModule } from './file-upload.module'; import { TsFileImageDimensionConstraints } from './image-dimension-constraints'; import { - TS_ACCEPTED_MIME_TYPES, TsFileAcceptedMimeTypes, + TS_ACCEPTED_MIME_TYPES, + TsFileAcceptedMimeTypes, } from './mime-types'; import { TsSelectedFile } from './selected-file'; @@ -36,8 +37,8 @@ const FILE_BLOB = new Blob( [fileContentsMock], { type: 'image/png' }, ); -FILE_BLOB.lastModifiedDate = new Date(); -FILE_BLOB.name = 'foo'; +FILE_BLOB['lastModifiedDate'] = new Date(); +FILE_BLOB['name'] = 'foo'; jest.spyOn(FILE_BLOB, 'size', 'get').mockReturnValue(3 * 1024); const FILE_MOCK = FILE_BLOB as File; @@ -45,25 +46,27 @@ const FILE_MOCK = FILE_BLOB as File; @Component({ template: ` `, }) class TestHostComponent { + public isDisabled = false; public mimeTypes: TsFileAcceptedMimeTypes | TsFileAcceptedMimeTypes[] | undefined = ['image/png', 'image/jpg']; public maxKb: number | undefined; public multiple = false; @@ -86,19 +89,13 @@ class TestHostComponent { } - - describe(`TsFileUploadComponent`, function() { let fixture: ComponentFixture; let hostComponent: TestHostComponent; let component: TsFileUploadComponent; const moduleDefinition: TestModuleMetadata = { - imports: [ - TsFileUploadModule, - ], - declarations: [ - TestHostComponent, - ], + imports: [TsFileUploadModule], + declarations: [TestHostComponent], }; configureTestBedWithoutReset(moduleDefinition); @@ -121,7 +118,7 @@ describe(`TsFileUploadComponent`, function() { class DummyFileReader { public addEventListener = jest.fn(); public readAsDataURL = jest.fn().mockImplementation(function(this: FileReader) { - this.onload({} as Event); + this.onload!({} as ProgressEvent); }); // eslint-disable-next-line max-len public result = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABIAQMAAABvIyEEAAAAA1BMVEXXbFn0Q9OUAAAADklEQVR4AWMYRmAUjAIAAtAAAaW+yXMAAAAASUVORK5CYII='; @@ -129,7 +126,6 @@ describe(`TsFileUploadComponent`, function() { // Not sure why any is needed (window as any).FileReader = jest.fn(() => new DummyFileReader); - class DummyImage { public _onload = () => {}; public set onload(fn) { @@ -155,6 +151,28 @@ describe(`TsFileUploadComponent`, function() { component = hostComponent.component; }); + describe(`isDisabled`, () => { + + test(`should disable the file upload`, () => { + component.ratioConstraints = ['2:1']; + fixture.detectChanges(); + const el = component['elementRef'].nativeElement; + const wrapper = el.querySelector('.c-file-upload'); + const button = el.querySelector('ts-button').querySelector('button'); + + expect(component.isDisabled).toEqual(false); + expect(wrapper.classList).not.toContain('c-file-upload--disabled'); + expect(button.getAttribute('disabled')).toEqual(null); + + hostComponent.isDisabled = true; + fixture.detectChanges(); + + expect(component.isDisabled).toEqual(true); + expect(wrapper.classList).toContain('c-file-upload--disabled'); + expect(button.getAttribute('disabled')).toEqual('true'); + }); + + }); describe(`accept`, () => { @@ -171,7 +189,6 @@ describe(`TsFileUploadComponent`, function() { }); - describe(`seedFile`, () => { test(`should seed the file and trigger all process'`, () => { @@ -188,11 +205,10 @@ describe(`TsFileUploadComponent`, function() { }); - describe(`buttonMessage`, () => { test(`should set the correct drop vs select message pluralized if neeeded`, () => { - const el = component.elementRef.nativeElement; + const el = component['elementRef'].nativeElement; hostComponent.multiple = true; dispatchMouseEvent(el, 'dragover'); fixture.detectChanges(); @@ -222,7 +238,6 @@ describe(`TsFileUploadComponent`, function() { }); - describe(`hideButton`, () => { test(`should hide the button from view`, () => { @@ -238,7 +253,6 @@ describe(`TsFileUploadComponent`, function() { }); - describe(`validation messages`, () => { test(`should show size validation message`, () => { @@ -253,7 +267,6 @@ describe(`TsFileUploadComponent`, function() { expect(uploadDiv.classes['c-file-upload--error']).toBeTruthy(); }); - test(`should show MIME type validation message`, () => { hostComponent.mimeTypes = 'text/csv'; fixture.detectChanges(); @@ -266,7 +279,6 @@ describe(`TsFileUploadComponent`, function() { expect(uploadDiv.classes['c-file-upload--error']).toBeTruthy(); }); - test(`should show image dimension validation message`, () => { hostComponent.constraints = [{ height: { @@ -288,7 +300,6 @@ describe(`TsFileUploadComponent`, function() { expect(uploadDiv.classes['c-file-upload--error']).toBeTruthy(); }); - test(`should show image ratio validation message`, () => { hostComponent.ratioConstraints = ['2:1']; fixture.detectChanges(); @@ -303,7 +314,6 @@ describe(`TsFileUploadComponent`, function() { }); - describe(`hints`, () => { test(`should set image hints`, () => { @@ -349,7 +359,6 @@ describe(`TsFileUploadComponent`, function() { expect(hints[2].nativeElement.textContent).toContain('Must be under 100kb'); }); - test(`should not set dimensions hint if constraints were not passed in`, () => { hostComponent.mimeTypes = ['image/jpg', 'image/png']; hostComponent.maxKb = 100; @@ -361,7 +370,6 @@ describe(`TsFileUploadComponent`, function() { expect(hints[1].nativeElement.textContent).toContain('Must be under 100kb'); }); - test(`should set csv hints`, () => { hostComponent.mimeTypes = 'text/csv'; hostComponent.maxKb = 100; @@ -381,7 +389,6 @@ describe(`TsFileUploadComponent`, function() { }); }); - describe(`removeFile`, () => { test(`should clear the file, clear validations and emit an event`, () => { @@ -401,135 +408,128 @@ describe(`TsFileUploadComponent`, function() { expect(component.file).toBeFalsy(); }); - - test(`should stop event propogation`, () => { + test(`should stop event propagation`, () => { component.seedFile = FILE_MOCK; fixture.detectChanges(); - component.preventAndStopEventPropagation = jest.fn(); const mouseEvent = createMouseEvent('click'); + Object.defineProperty(mouseEvent, 'preventDefault', { value: jest.fn() }); + Object.defineProperty(mouseEvent, 'stopPropagation', { value: jest.fn() }); + component.removeFile(mouseEvent); - expect(component.preventAndStopEventPropagation).toHaveBeenCalledWith(mouseEvent); + expect(mouseEvent.preventDefault).toHaveBeenCalled(); + expect(mouseEvent.stopPropagation).toHaveBeenCalled(); }); }); - describe(`setUpNewFile`, () => { test(`should not continue if no file is passed in`, () => { - component.setValidationMessages = jest.fn(); - component.setUpNewFile(undefined as any); + component['setValidationMessages'] = jest.fn(); + component['setUpNewFile'](undefined as any); - expect(component.setValidationMessages).not.toHaveBeenCalled(); + expect(component['setValidationMessages']).not.toHaveBeenCalled(); }); }); - describe(`setValidationMessages`, () => { test(`should do nothing if no file was passed in`, () => { component.formControl.setErrors = jest.fn(); - component.setValidationMessages(undefined); + component['setValidationMessages'](undefined); expect(component.formControl.setErrors).not.toHaveBeenCalled(); }); }); - describe(`updateVirtualFileInputAttrs`, () => { test(`should add and remove the multiple attr`, () => { - expect(component.virtualFileInput.getAttribute('multiple')).toBeFalsy(); + expect(component['virtualFileInput'].getAttribute('multiple')).toBeFalsy(); hostComponent.multiple = true; fixture.detectChanges(); - expect(component.virtualFileInput.getAttribute('multiple')).toBeTruthy(); + expect(component['virtualFileInput'].getAttribute('multiple')).toBeTruthy(); hostComponent.multiple = false; fixture.detectChanges(); - expect(component.virtualFileInput.getAttribute('multiple')).toBeFalsy(); + expect(component['virtualFileInput'].getAttribute('multiple')).toBeFalsy(); }); - test(`should add and remove the accept attr`, () => { - expect(component.virtualFileInput.getAttribute('accept')).toBeFalsy(); + expect(component['virtualFileInput'].getAttribute('accept')).toBeFalsy(); hostComponent.mimeTypes = 'text/csv'; fixture.detectChanges(); - expect(component.virtualFileInput.getAttribute('accept')).toEqual('text/csv'); + expect(component['virtualFileInput'].getAttribute('accept')).toEqual('text/csv'); hostComponent.mimeTypes = undefined; fixture.detectChanges(); - expect(component.virtualFileInput.getAttribute('accept')) + expect(component['virtualFileInput'].getAttribute('accept')) .toEqual('text/csv,image/jpeg,image/jpg,image/png,image/gif,video/mp4,video/x-flv,video/webm,video/quicktime,video/mpeg'); }); }); - describe(`HostListeners`, () => { test(`should handle dragover`, () => { - component.preventAndStopEventPropagation = jest.fn(); - dispatchMouseEvent(component.elementRef.nativeElement, 'dragover'); + component['preventAndStopEventPropagation'] = jest.fn(); + dispatchMouseEvent(component['elementRef'].nativeElement, 'dragover'); fixture.detectChanges(); const foundClass = fixture.debugElement.query(By.css('.c-file-upload--drag')); expect(foundClass).toBeTruthy(); - expect(component.preventAndStopEventPropagation).toHaveBeenCalledWith(expect.any(Event)); + expect(component['preventAndStopEventPropagation']).toHaveBeenCalledWith(expect.any(Event)); expect(hostComponent.userDragBegin).toHaveBeenCalled(); }); - test(`should handle dragleave`, () => { - component.preventAndStopEventPropagation = jest.fn(); - dispatchMouseEvent(component.elementRef.nativeElement, 'dragleave'); + component['preventAndStopEventPropagation'] = jest.fn(); + dispatchMouseEvent(component['elementRef'].nativeElement, 'dragleave'); fixture.detectChanges(); const foundClass = fixture.debugElement.query(By.css('.c-file-upload--drag')); expect(foundClass).toBeFalsy(); - expect(component.preventAndStopEventPropagation).toHaveBeenCalledWith(expect.any(Event)); + expect(component['preventAndStopEventPropagation']).toHaveBeenCalledWith(expect.any(Event)); expect(hostComponent.userDragEnd).toHaveBeenCalled(); }); - test(`should handle drop`, () => { component.dragInProgress = true; - component.preventAndStopEventPropagation = jest.fn(); - component.collectFilesFromEvent = jest.fn(); + component['preventAndStopEventPropagation'] = jest.fn(); + component['collectFilesFromEvent'] = jest.fn(); const event = createFakeEvent('drop'); fixture.detectChanges(); expect(fixture.debugElement.query(By.css('.c-file-upload--drag'))).toBeTruthy(); - component.elementRef.nativeElement.dispatchEvent(event); + component['elementRef'].nativeElement.dispatchEvent(event); fixture.detectChanges(); expect(fixture.debugElement.query(By.css('.c-file-upload--drag'))).toBeFalsy(); - expect(component.preventAndStopEventPropagation).toHaveBeenCalled(); - expect(component.collectFilesFromEvent).toHaveBeenCalled(); + expect(component['preventAndStopEventPropagation']).toHaveBeenCalled(); + expect(component['collectFilesFromEvent']).toHaveBeenCalled(); }); - test(`should handle click`, () => { - component.virtualFileInput.click = jest.fn(); - dispatchMouseEvent(component.elementRef.nativeElement, 'click'); + component['virtualFileInput'].click = jest.fn(); + dispatchMouseEvent(component['elementRef'].nativeElement, 'click'); - expect(component.virtualFileInput.click).toHaveBeenCalled(); + expect(component['virtualFileInput'].click).toHaveBeenCalled(); }); - describe(`keypress`, () => { let el: HTMLElement; beforeEach(() => { - el = component.elementRef.nativeElement; + el = component['elementRef'].nativeElement; component.promptForFiles = jest.fn(); el.blur = jest.fn(); }); @@ -541,7 +541,6 @@ describe(`TsFileUploadComponent`, function() { expect(el.blur).toHaveBeenCalled(); }); - test(`should do nothing if the key pressed was not Enter`, () => { dispatchKeyboardEvent(el, 'keydown', KEYS.A); @@ -552,20 +551,22 @@ describe(`TsFileUploadComponent`, function() { }); - describe(`collectFilesFromEvent`, () => { + beforeEach(() => { + component['setUpNewFile'] = jest.fn(); + }); + test(`should throw an error if no files exist in the dataTransfer object`, () => { const event = createFakeEvent('DragEvent') as DragEvent; const dataTransfer = { files: [] }; Object.defineProperty(event, 'dataTransfer', { value: dataTransfer }); - component.setUpNewFile = jest.fn(); expect(() => { - component.collectFilesFromEvent(event); + component['collectFilesFromEvent'](event); }).toThrowError(); fixture.detectChanges(); - expect(component.setUpNewFile).not.toHaveBeenCalled(); + expect(component['setUpNewFile']).not.toHaveBeenCalled(); expect(hostComponent.handleFile).not.toHaveBeenCalled(); }); @@ -574,57 +575,50 @@ describe(`TsFileUploadComponent`, function() { const event = createFakeEvent('Event'); const input = document.createElement('input'); Object.defineProperty(event, 'target', { value: input }); - component.setUpNewFile = jest.fn(); expect(() => { - component.collectFilesFromEvent(event); + component['collectFilesFromEvent'](event); }).toThrowError(); fixture.detectChanges(); - expect(component.setUpNewFile).not.toHaveBeenCalled(); + expect(component['setUpNewFile']).not.toHaveBeenCalled(); expect(hostComponent.handleFile).not.toHaveBeenCalled(); }); - test(`should collect a file from a drag/drop event`, () => { const event = createFakeEvent('DragEvent') as DragEvent; const dataTransfer = { files: [FILE_MOCK] }; Object.defineProperty(event, 'dataTransfer', { value: dataTransfer }); - component.setUpNewFile = jest.fn(); fixture.detectChanges(); - component.collectFilesFromEvent(event); + component['collectFilesFromEvent'](event); fixture.detectChanges(); - expect(component.setUpNewFile).toHaveBeenCalledWith(expect.any(TsSelectedFile)); + expect(component['setUpNewFile']).toHaveBeenCalledWith(expect.any(TsSelectedFile)); expect(hostComponent.handleFile).toHaveBeenCalledWith(expect.any(TsSelectedFile)); expect(hostComponent.formControl.value).toEqual(FILE_MOCK); }); - test(`should collect a file from an input change (manual selection)`, () => { const event = createFakeEvent('Event'); const input = document.createElement('input'); Object.defineProperty(input, 'files', { value: [FILE_MOCK] }); Object.defineProperty(event, 'target', { value: input }); - component.setUpNewFile = jest.fn(); - component.collectFilesFromEvent(event); + component['collectFilesFromEvent'](event); fixture.detectChanges(); - expect(component.setUpNewFile).toHaveBeenCalledWith(expect.any(TsSelectedFile)); + expect(component['setUpNewFile']).toHaveBeenCalledWith(expect.any(TsSelectedFile)); expect(hostComponent.handleFile).toHaveBeenCalledWith(expect.any(TsSelectedFile)); }); - test(`should collect emit when multiple files are selected`, () => { const event = createFakeEvent('DragEvent') as DragEvent; const dataTransfer = { files: [FILE_MOCK, FILE_MOCK] }; Object.defineProperty(event, 'dataTransfer', { value: dataTransfer }); - component.setUpNewFile = jest.fn(); - component.collectFilesFromEvent(event); + component['collectFilesFromEvent'](event); fixture.detectChanges(); expect(hostComponent.handleMultipleFiles).toHaveBeenCalled(); expect(hostComponent.handleFile).not.toHaveBeenCalled(); - expect(component.setUpNewFile).not.toHaveBeenCalled(); + expect(component['setUpNewFile']).not.toHaveBeenCalled(); }); }); @@ -633,44 +627,42 @@ describe(`TsFileUploadComponent`, function() { describe(`ngOnDestroy`, () => { test(`should remove the event listener`, () => { - component.onVirtualInputElementChange = jest.fn(); - component.dropProtectionService.remove = jest.fn(); + component['onVirtualInputElementChange'] = jest.fn(); + component['dropProtectionService'].remove = jest.fn(); component.ngOnDestroy(); const event = createFakeEvent('change'); - component.virtualFileInput.dispatchEvent(event); + component['virtualFileInput'].dispatchEvent(event); - expect(component.onVirtualInputElementChange).not.toHaveBeenCalled(); - expect(component.dropProtectionService.remove).toHaveBeenCalled(); + expect(component['onVirtualInputElementChange']).not.toHaveBeenCalled(); + expect(component['dropProtectionService'].remove).toHaveBeenCalled(); }); }); - describe(`virtualFileInput.change`, () => { test(`should trigger the file handler`, () => { - component.collectFilesFromEvent = jest.fn(); + component['collectFilesFromEvent'] = jest.fn(); // Wire up bindings component.ngAfterContentInit(); const event = createFakeEvent('change'); - component.virtualFileInput.dispatchEvent(event); + component['virtualFileInput'].dispatchEvent(event); - expect(component.collectFilesFromEvent).toHaveBeenCalled(); - expect(component.virtualFileInput.value).toEqual(''); + expect(component['collectFilesFromEvent']).toHaveBeenCalled(); + expect(component['virtualFileInput'].value).toEqual(''); }); }); - describe(`preventAndStopEventPropagation`, () => { - test(`should both prevent and stop event propogation`, () => { + test(`should both prevent and stop event propagation`, () => { const event = createFakeEvent('fake'); Object.defineProperties(event, { preventDefault: { value: jest.fn() }, stopPropagation: { value: jest.fn() }, }); - component.preventAndStopEventPropagation(event); + component['preventAndStopEventPropagation'](event); expect(event.preventDefault).toHaveBeenCalled(); expect(event.stopPropagation).toHaveBeenCalled(); @@ -678,19 +670,17 @@ describe(`TsFileUploadComponent`, function() { }); - describe(`ngOnInit`, () => { test(`should enable dropProtectionService`, () => { - component.dropProtectionService.add = jest.fn(); + component['dropProtectionService'].add = jest.fn(); component.ngOnInit(); - expect(component.dropProtectionService.add).toHaveBeenCalled(); + expect(component['dropProtectionService'].add).toHaveBeenCalled(); }); }); - describe(`theme`, () => { test(`should set the theme`, () => { @@ -702,17 +692,46 @@ describe(`TsFileUploadComponent`, function() { }); + describe(`formControl`, () => { + + test(`should fall back to internal control if one is not passed in`, () => { + component.formControl = undefined as any; + expect(component.formControl).toBeTruthy(); + }); + + }); + + describe(`id`, () => { + + test(`should support a custom ID and fall back to UID`, () => { + component.id = 'foo'; + fixture.detectChanges(); + const wrapper = component['elementRef'].nativeElement.querySelector('#foo'); + + expect(component.id).toEqual('foo'); + expect(wrapper).toBeTruthy(); + + component.id = undefined as any; + fixture.detectChanges(); + expect(component.id).toEqual(expect.stringContaining('ts-file-upload-')); + }); + + }); + + // NOTE: Currently this must be our last test - something may not be getting reset properly describe(`ratioConstraint format`, () => { - test(`should throw error if ratioContraint is not in right format`, () => { + + test(`should throw error if ratio constraint is not in right format`, () => { expect(() => { try { - hostComponent.ratioConstraints = '5' as any; + hostComponent.ratioConstraints = '5:v' as any; fixture.detectChanges(); } catch (e) { throw new Error(e); } }).toThrowError(); }); + }); }); diff --git a/terminus-ui/file-upload/src/file-upload.component.ts b/terminus-ui/file-upload/src/file-upload.component.ts index a9f604b89..5b8dd896f 100644 --- a/terminus-ui/file-upload/src/file-upload.component.ts +++ b/terminus-ui/file-upload/src/file-upload.component.ts @@ -5,7 +5,6 @@ import { Component, ElementRef, EventEmitter, - HostBinding, HostListener, Input, isDevMode, @@ -125,7 +124,7 @@ export class TsFileUploadComponent extends TsReactiveFormBaseComponent implement /** * Define the default component ID */ - protected _uid = `ts-file-upload-${nextUniqueId++}`; + protected uid = `ts-file-upload-${nextUniqueId++}`; /** * A flag that represents an in-progress drag movement @@ -145,13 +144,7 @@ export class TsFileUploadComponent extends TsReactiveFormBaseComponent implement /** * Store reference to the generated file input */ - private virtualFileInput: HTMLInputElement; - - /** - * Reflect the ID back to the DOM - */ - @HostBinding('attr.id') - public publicID: string = this.id; + private readonly virtualFileInput: HTMLInputElement; /** * Provide access to the file preview element @@ -163,11 +156,7 @@ export class TsFileUploadComponent extends TsReactiveFormBaseComponent implement * Get the file select button text */ public get buttonMessage(): string { - if (this.dragInProgress) { - return `Drop File${this.multiple ? 's' : ''}`; - } - return `Select File${this.multiple ? 's' : ''}`; - + return this.dragInProgress ? `Drop File${this.multiple ? 's' : ''}` : `Select File${this.multiple ? 's' : ''}`; } /** @@ -265,8 +254,6 @@ export class TsFileUploadComponent extends TsReactiveFormBaseComponent implement /** * Define if the 'select files' button should be visible. DO NOT USE. - * - * TODO: This should be removed once UX/Product decide if they want the button. */ @Input() public hideButton = false; @@ -276,23 +263,25 @@ export class TsFileUploadComponent extends TsReactiveFormBaseComponent implement */ @Input() public set id(value: string) { - this._id = value || this._uid; + this._id = value || this.uid; } public get id(): string { return this._id; } - protected _id!: string; + private _id: string = this.uid; + + /** + * Define if the component is disabled + */ + @Input() + public isDisabled = false; /** * Define the maximum file size in kilobytes */ @Input() public set maximumKilobytesPerFile(value: number) { - if (!value) { - return; - } - - this._maximumKilobytesPerFile = value; + this._maximumKilobytesPerFile = value || MAXIMUM_KILOBYTES_PER_FILE; } public get maximumKilobytesPerFile(): number { return this._maximumKilobytesPerFile; @@ -308,7 +297,7 @@ export class TsFileUploadComponent extends TsReactiveFormBaseComponent implement for (const value of values) { const v = value.split(':'); const minPartsForValidRatio = 2; - if ((v.length !== minPartsForValidRatio) || (!isNumber(v[0]) && !isNumber(v[1]))) { + if ((v.length !== minPartsForValidRatio) || (!isNumber(v[0]) || !isNumber(v[1]))) { throw new Error('TsFileUploadComponent: An array of image ratios should be formatted as ["1:2", "3:4"]'); } } @@ -320,7 +309,6 @@ export class TsFileUploadComponent extends TsReactiveFormBaseComponent implement } private _ratioConstraints: Array | undefined; - /** * Define if multiple files may be uploaded */ @@ -393,59 +381,71 @@ export class TsFileUploadComponent extends TsReactiveFormBaseComponent implement * Event emitted when the user's cursor enters the field while dragging a file */ @Output() - public readonly enter: EventEmitter = new EventEmitter(); + public readonly enter = new EventEmitter(); /** * Event emitted when the user's cursor exits the field while dragging a file */ @Output() - public readonly exit: EventEmitter = new EventEmitter(); + public readonly exit = new EventEmitter(); /** * Event emitted when the user drops or selects a file */ @Output() - public readonly selected: EventEmitter = new EventEmitter(); + public readonly selected = new EventEmitter(); /** * Event emitted when the user drops or selects multiple files */ @Output() - public readonly selectedMultiple: EventEmitter = new EventEmitter(); + public readonly selectedMultiple = new EventEmitter(); /** * Event emitted when the user clears a loaded file */ @Output() - public readonly cleared: EventEmitter = new EventEmitter(); + public readonly cleared = new EventEmitter(); /** * HostListeners */ @HostListener('dragover', ['$event']) public handleDragover(event: TsFileUploadDragEvent) { - this.preventAndStopEventPropagation(event); - this.enter.emit(true); - this.dragInProgress = true; + // istanbul ignore else + if (!this.isDisabled) { + this.preventAndStopEventPropagation(event); + this.enter.emit(true); + this.dragInProgress = true; + } } @HostListener('dragleave', ['$event']) public handleDragleave(event: TsFileUploadDragEvent) { - this.preventAndStopEventPropagation(event); - this.exit.emit(true); - this.dragInProgress = false; + // istanbul ignore else + if (!this.isDisabled) { + this.preventAndStopEventPropagation(event); + this.exit.emit(true); + this.dragInProgress = false; + } } @HostListener('drop', ['$event']) public handleDrop(event: TsFileUploadDragEvent) { - this.preventAndStopEventPropagation(event); - this.dragInProgress = false; - this.collectFilesFromEvent(event); + // istanbul ignore else + if (!this.isDisabled) { + this.preventAndStopEventPropagation(event); + this.dragInProgress = false; + this.collectFilesFromEvent(event); + } } @HostListener('click') public handleClick() { - this.promptForFiles(); + // istanbul ignore else + if (!this.isDisabled) { + this.promptForFiles(); + } } @@ -457,9 +457,6 @@ export class TsFileUploadComponent extends TsReactiveFormBaseComponent implement ) { super(); this.virtualFileInput = this.createFileInput(); - - // Force setter to be called in case the ID was not specified. - this.id = this.id; } /** @@ -500,7 +497,7 @@ export class TsFileUploadComponent extends TsReactiveFormBaseComponent implement /** - * Update the virtual file imput when the change event is fired + * Update the virtual file input when the change event is fired */ public ngAfterContentInit(): void { this.virtualFileInput.addEventListener('change', this.onVirtualInputElementChange.bind(this)); @@ -669,13 +666,18 @@ export class TsFileUploadComponent extends TsReactiveFormBaseComponent implement * @param event - The event */ private onVirtualInputElementChange(event: Event): void { - this.collectFilesFromEvent(event); - this.virtualFileInput.value = ''; + // istanbul ignore else + if (!this.isDisabled) { + this.collectFilesFromEvent(event); + this.virtualFileInput.value = ''; + } } /* - * Stops event propogation + * Stops event propagation + * + * NOTE: Making this static seems to break our tests. */ private preventAndStopEventPropagation(event: Event): void { event.preventDefault(); diff --git a/terminus-ui/file-upload/src/image-dimension-constraints.ts b/terminus-ui/file-upload/src/image-dimension-constraints.ts index 5ac8e6fc4..546419487 100644 --- a/terminus-ui/file-upload/src/image-dimension-constraints.ts +++ b/terminus-ui/file-upload/src/image-dimension-constraints.ts @@ -1,6 +1,6 @@ /** - * An indiviual size constraint + * An individual size constraint */ export interface TsFileImageDimensionContraint { height: { diff --git a/terminus-ui/file-upload/src/selected-file.spec.ts b/terminus-ui/file-upload/src/selected-file.spec.ts index 13d79c681..e76614a71 100644 --- a/terminus-ui/file-upload/src/selected-file.spec.ts +++ b/terminus-ui/file-upload/src/selected-file.spec.ts @@ -1,4 +1,3 @@ - import { TsFileImageDimensionConstraints } from './image-dimension-constraints'; import { TsFileAcceptedMimeTypes } from './mime-types'; import { TsSelectedFile } from './selected-file'; @@ -33,8 +32,8 @@ const FILE_BLOB = new Blob( ['data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABIAQMAAABvIyEEAAAAA1BMVEXXbFn0Q9OUAAAADklEQVR4AWMYRmAUjAIAAtAAAaW+yXMAAAAASUVORK5CYII='], { type: 'image/png' }, ); -FILE_BLOB.lastModifiedDate = new Date(); -FILE_BLOB.name = 'foo'; +FILE_BLOB['lastModifiedDate'] = new Date(); +FILE_BLOB['name'] = 'foo'; jest.spyOn(FILE_BLOB, 'size', 'get').mockReturnValue(3 * 1024); const FILE_MOCK = FILE_BLOB as File; @@ -43,8 +42,8 @@ const FILE_CSV_BLOB = new Blob( ['my csv value'], { type: 'text/csv' }, ); -FILE_CSV_BLOB.lastModifiedDate = new Date(); -FILE_CSV_BLOB.name = 'myCSV'; +FILE_CSV_BLOB['lastModifiedDate'] = new Date(); +FILE_CSV_BLOB['name'] = 'myCSV'; jest.spyOn(FILE_CSV_BLOB, 'size', 'get').mockReturnValue(3 * 1024); const FILE_CSV_MOCK = FILE_CSV_BLOB as File; @@ -53,14 +52,12 @@ const FILE_VIDEO_BLOB = new Blob( ['my video value'], { type: 'video/mp4' }, ); -FILE_VIDEO_BLOB.lastModifiedDate = new Date(); -FILE_VIDEO_BLOB.name = 'myVideo'; +FILE_VIDEO_BLOB['lastModifiedDate'] = new Date(); +FILE_VIDEO_BLOB['name'] = 'myVideo'; jest.spyOn(FILE_VIDEO_BLOB, 'size', 'get').mockReturnValue(3 * 1024); const FILE_VIDEO_MOCK = FILE_VIDEO_BLOB as File; - - describe(`TsSelectedFile`, function() { const createFile = ( file = FILE_MOCK, @@ -79,7 +76,6 @@ describe(`TsSelectedFile`, function() { ratio, ); - // Mock `FileReader` and `Image`: beforeEach(() => { // Mock FileReader @@ -114,10 +110,8 @@ describe(`TsSelectedFile`, function() { } } (window as any).Image = jest.fn(() => new DummyImage()); - }); - describe(`constructor`, () => { test(`should set top-level items and validations`, () => { @@ -135,7 +129,6 @@ describe(`TsSelectedFile`, function() { expect.assertions(6); }); - test(`should set top-level items and validations for videos`, done => { const file = createFile(FILE_VIDEO_MOCK, undefined, ['video/mp4']); file.fileLoaded$.subscribe(f => { @@ -154,7 +147,6 @@ describe(`TsSelectedFile`, function() { }); - describe(`width`, () => { test(`should return the width or zero`, () => { @@ -167,7 +159,6 @@ describe(`TsSelectedFile`, function() { }); - describe(`height`, () => { test(`should return the height or zero`, () => { @@ -180,7 +171,6 @@ describe(`TsSelectedFile`, function() { }); - describe(`isCSV`, () => { test(`should return true is the file is a CSV`, () => { @@ -195,7 +185,6 @@ describe(`TsSelectedFile`, function() { }); - describe(`isImage`, () => { test(`should return true is the file is an image`, () => { @@ -209,7 +198,6 @@ describe(`TsSelectedFile`, function() { }); - describe(`isVideo`, () => { test(`should return true is the file is a video`, () => { @@ -224,7 +212,6 @@ describe(`TsSelectedFile`, function() { }); - describe(`fileContents`, () => { test(`should return the FileReader result`, () => { @@ -257,7 +244,7 @@ describe(`TsSelectedFile`, function() { public onload = jest.fn(); public addEventListener = jest.fn(); public readAsDataURL = jest.fn().mockImplementation(function(this: FileReader) { - this.onload({} as Event); + this.onload!({} as ProgressEvent); }); public result = str2ab(FILE_BLOB); } @@ -277,7 +264,7 @@ describe(`TsSelectedFile`, function() { public onload = jest.fn(); public addEventListener = jest.fn(); public readAsDataURL = jest.fn().mockImplementation(function(this: FileReader) { - this.onload({} as Event); + this.onload!({} as ProgressEvent); }); public result = str2ab(FILE_CSV_BLOB); } @@ -293,11 +280,8 @@ describe(`TsSelectedFile`, function() { }); }); - - }); - describe(`isValid`, () => { const file = createFile(); @@ -334,7 +318,6 @@ describe(`TsSelectedFile`, function() { }); - describe(`determineImageDimensions`, () => { test(`should set validation to true and exit if the file is not an image`, done => { @@ -351,7 +334,6 @@ describe(`TsSelectedFile`, function() { expect.assertions(4); }); - test(`should still seed the FileReader for non-image files`, done => { const file = createFile(FILE_CSV_MOCK); file.fileLoaded$.subscribe(f => { @@ -363,7 +345,6 @@ describe(`TsSelectedFile`, function() { expect.assertions(1); }); - test(`should set dimensions and call callback`, () => { createFile().fileLoaded$.subscribe(f => { if (f) { @@ -378,28 +359,25 @@ describe(`TsSelectedFile`, function() { }); - describe(`validateImageRatio`, () => { test(`should return true if no constraints exist`, () => { const file = createFile(); - expect(file.validateImageRatio(undefined)).toEqual(true); + expect(file['validateImageRatio'](undefined)).toEqual(true); }); - test(`should return true if ratio are valid`, () => { const file = createFile(); - const result = file.validateImageRatio([{ + const result = file['validateImageRatio']([{ widthRatio: 1, heightRatio: 1, }]); expect(result).toEqual(true); }); - test(`should return false if ratio are not valid`, () => { const file = createFile(); - const result = file.validateImageRatio([{ + const result = file['validateImageRatio']([{ widthRatio: 2, heightRatio: 1, }]); @@ -412,17 +390,15 @@ describe(`TsSelectedFile`, function() { test(`should return true if no constraints exist`, () => { const file = createFile(); - expect(file.validateImageDimensions(undefined)).toEqual(true); + expect(file['validateImageDimensions'](undefined)).toEqual(true); }); - test(`should return true if dimensions are valid`, () => { const file = createFile(); - const result = file.validateImageDimensions(CONSTRAINTS_MOCK); + const result = file['validateImageDimensions'](CONSTRAINTS_MOCK); expect(result).toEqual(true); }); - test(`should return false if dimensions are not valid`, () => { const file = createFile(); const constraints = [ @@ -447,7 +423,7 @@ describe(`TsSelectedFile`, function() { }, }, ]; - const result = file.validateImageDimensions(constraints); + const result = file['validateImageDimensions'](constraints); expect(result).toEqual(false); }); diff --git a/terminus-ui/file-upload/src/selected-file.ts b/terminus-ui/file-upload/src/selected-file.ts index 919926b1b..0dfdb2480 100644 --- a/terminus-ui/file-upload/src/selected-file.ts +++ b/terminus-ui/file-upload/src/selected-file.ts @@ -273,10 +273,7 @@ export class TsSelectedFile { */ private isSame(number1: number, number2: number): boolean { const minimumAmountToConsiderMatch = .001; - if (Math.abs((number1 - number2) / number1) < minimumAmountToConsiderMatch) { - return true; - } - return false; + return Math.abs((number1 - number2) / number1) < minimumAmountToConsiderMatch; } }