From 95f772fbb03f0020aabd73ad177373ee6a5c041e Mon Sep 17 00:00:00 2001 From: Benjamin Charity Date: Thu, 25 Apr 2019 13:05:16 -0400 Subject: [PATCH] chore: integrate new lint rules and fix all issues --- .eslintrc | 3 - .eslintrc.ci.js | 18 + .eslintrc.js | 18 + .../autocomplete/autocomplete.component.ts | 24 +- demo/app/components/chart/chart.component.ts | 58 +-- .../file-upload/file-upload.component.ts | 2 +- .../login-form/login-form.component.html | 5 + .../login-form/login-form.component.ts | 11 + .../navigation/navigation.component.ts | 4 +- .../paginator/paginator.component.html | 2 +- .../paginator/paginator.component.ts | 23 +- .../components/select/select.component.html | 4 +- demo/app/components/select/select.module.ts | 12 +- .../spacing/spacing-styles.component.html | 48 ++ .../spacing/spacing-styles.component.ts | 51 +- .../components/spacing/spacing.component.scss | 4 + .../components/spacing/spacing.component.ts | 7 +- demo/tsconfig.app.json | 7 +- demo/tslint.json | 15 + package.json | 49 +- .../src/autocomplete.component.spec.ts | 50 +- .../src/autocomplete.component.ts | 99 ++-- .../autofocus/src/autofocus.directive.ts | 9 +- .../button/src/button.component.spec.ts | 460 +++++++++--------- terminus-ui/button/src/button.component.ts | 44 +- .../card/src/card-title.directive.spec.ts | 4 +- terminus-ui/card/src/card-title.directive.ts | 8 +- terminus-ui/card/src/card.component.spec.ts | 8 +- terminus-ui/card/src/card.component.ts | 8 +- terminus-ui/chart/src/amcharts.service.ts | 2 + terminus-ui/chart/src/chart-type-check.ts | 79 +++ terminus-ui/chart/src/chart.component.spec.ts | 157 ++++-- terminus-ui/chart/src/chart.component.ts | 69 +-- terminus-ui/chart/src/chart.module.ts | 5 +- .../checkbox/src/checkbox.component.spec.ts | 5 +- .../checkbox/src/checkbox.component.ts | 10 +- .../checkbox/testing/src/test-helpers.ts | 6 +- .../src/confirmation.directive.spec.ts | 81 +-- .../src/confirmation.directive.ts | 62 ++- terminus-ui/copy/src/copy.component.spec.ts | 4 +- terminus-ui/copy/src/copy.component.ts | 12 +- .../csv-entry/src/csv-entry.component.html | 9 +- .../csv-entry/src/csv-entry.component.spec.ts | 229 ++++++--- .../csv-entry/src/csv-entry.component.ts | 190 +++++--- .../src/date-range.component.spec.ts | 37 +- .../date-range/src/date-range.component.ts | 16 +- .../date-range/testing/src/test-helpers.ts | 6 +- .../src/accordion/accordion-base.ts | 4 +- .../src/accordion/accordion.component.spec.ts | 17 +- .../src/accordion/accordion.component.ts | 10 +- .../src/expansion-animations.ts | 32 +- .../src/expansion-panel-action-row.ts | 11 +- .../src/expansion-panel-content.directive.ts | 1 + .../src/expansion-panel.component.spec.ts | 5 +- .../src/expansion-panel.component.ts | 31 +- ...ion-panel-trigger-description.component.ts | 9 +- ...expansion-panel-trigger-title.component.ts | 9 +- .../expansion-panel-trigger.component.ts | 41 +- .../testing/src/test-components.ts | 10 +- .../testing/src/test-helpers.ts | 4 +- .../src/drop-protection.service.ts | 6 +- .../src/file-upload.component.html | 2 +- .../src/file-upload.component.spec.ts | 326 +++++++------ .../file-upload/src/file-upload.component.ts | 83 ++-- .../file-upload/src/selected-file.spec.ts | 128 +++-- terminus-ui/file-upload/src/selected-file.ts | 40 +- .../form-field/src/form-field-control.ts | 29 +- .../src/form-field.component.spec.ts | 18 +- .../form-field/src/form-field.component.ts | 9 +- .../src/icon-button.component.spec.ts | 5 +- .../icon-button/src/icon-button.component.ts | 2 +- terminus-ui/icon/src/custom-icons/csv.ts | 2 +- terminus-ui/icon/src/custom-icons/engage.ts | 2 +- .../icon/src/custom-icons/lightbulb.ts | 2 +- terminus-ui/icon/src/custom-icons/logo.ts | 2 +- .../icon/src/custom-icons/logo_color.ts | 2 +- terminus-ui/icon/src/icon.component.spec.ts | 5 +- terminus-ui/icon/src/icon.component.ts | 6 +- terminus-ui/input/src/date-adapter.ts | 6 +- terminus-ui/input/src/input-value-accessor.ts | 1 + terminus-ui/input/src/input.component.spec.ts | 50 +- terminus-ui/input/src/input.component.ts | 133 +++-- .../input/testing/src/test-components.ts | 2 +- terminus-ui/input/testing/src/test-helpers.ts | 2 +- terminus-ui/link/src/link.component.spec.ts | 15 +- terminus-ui/link/src/link.component.ts | 6 +- .../src/loading-overlay.component.html | 19 + .../src/loading-overlay.component.ts | 24 +- .../src/loading-overlay.directive.spec.ts | 4 +- .../src/loading-overlay.directive.ts | 5 +- .../login-form/src/login-form.component.html | 12 +- .../src/login-form.component.spec.ts | 82 +++- .../login-form/src/login-form.component.ts | 59 ++- .../logo/src/logo-types/full-account-hub.ts | 2 +- .../logo/src/logo-types/full-gradient.ts | 2 +- terminus-ui/logo/src/logo-types/full-solid.ts | 2 +- .../logo/src/logo-types/mark-gradient.ts | 40 +- terminus-ui/logo/src/logo-types/mark-solid.ts | 28 +- terminus-ui/logo/src/logo.component.spec.ts | 15 +- terminus-ui/logo/src/logo.component.ts | 25 +- terminus-ui/menu/src/menu.component.ts | 6 +- .../navigation/src/navigation.component.html | 14 +- .../src/navigation.component.spec.ts | 8 +- .../navigation/src/navigation.component.ts | 57 ++- .../paginator/src/paginator.component.spec.ts | 18 +- .../paginator/src/paginator.component.ts | 107 ++-- terminus-ui/pipes/src/date/date.pipe.ts | 24 +- .../src/round-number/round-number.pipe.ts | 4 +- .../src/sentence-case/sentence-case.pipe.ts | 4 +- .../pipes/src/time-ago/time-ago.pipe.ts | 4 +- .../pipes/src/title-case/title-case.pipe.ts | 8 +- .../pipes/src/truncate/truncate.pipe.ts | 21 +- .../src/radio-group.component.html | 4 +- .../src/radio-group.component.spec.ts | 34 +- .../radio-group/src/radio-group.component.ts | 62 ++- .../radio-group/src/radio-group.module.ts | 4 +- .../radio-group/testing/src/test-helpers.ts | 6 +- .../src/scrollbars.component.spec.ts | 44 +- .../scrollbars/src/scrollbars.component.ts | 37 +- terminus-ui/search/src/search.component.html | 2 +- .../search/src/search.component.spec.ts | 26 +- terminus-ui/search/src/search.component.ts | 32 +- .../autocomplete-panel.component.ts | 14 +- .../autocomplete-trigger.directive.ts | 94 ++-- .../select/src/optgroup/optgroup.component.ts | 8 +- .../src/option/option-utilities.spec.ts | 3 +- .../select/src/option/option-utilities.ts | 6 +- .../select/src/option/option.component.ts | 22 +- terminus-ui/select/src/select-animations.ts | 4 +- .../select/src/select-trigger.component.ts | 4 +- terminus-ui/select/src/select.component.html | 2 +- .../select/src/select.component.spec.ts | 69 ++- terminus-ui/select/src/select.component.ts | 230 +++++---- terminus-ui/select/src/select.module.ts | 4 +- .../select/testing/src/test-components.ts | 86 +++- .../select/testing/src/test-helpers.ts | 44 +- terminus-ui/sort/src/sort-animations.ts | 66 ++- terminus-ui/sort/src/sort-header-intl.ts | 11 +- terminus-ui/sort/src/sort-header.component.ts | 37 +- terminus-ui/sort/src/sort.directive.ts | 34 +- terminus-ui/sort/src/sort.spec.ts | 19 +- terminus-ui/spacing/src/spacing.constant.ts | 28 +- .../src/vertical-spacing.directive.spec.ts | 8 +- terminus-ui/src/public-api.ts | 1 + terminus-ui/table/src/cell.ts | 34 +- terminus-ui/table/src/row.ts | 25 +- .../table/src/table-data-source.spec.ts | 9 +- terminus-ui/table/src/table-data-source.ts | 19 +- terminus-ui/table/src/table.component.html | 7 + terminus-ui/table/src/table.component.spec.ts | 112 +++-- terminus-ui/table/src/table.component.ts | 10 +- terminus-ui/tabs/src/body/tab-animations.ts | 12 +- .../tabs/src/body/tab-body.component.ts | 40 +- .../tabs/src/body/tab-content.directive.ts | 1 + .../collection/tab-collection.component.html | 14 +- .../tab-collection.component.spec.ts | 60 ++- .../collection/tab-collection.component.ts | 77 ++- .../src/header/tab-header.component.spec.ts | 83 +++- .../tabs/src/header/tab-header.component.ts | 45 +- .../src/ink-bar/ink-bar.component.spec.ts | 5 +- .../tabs/src/ink-bar/ink-bar.component.ts | 9 +- .../src/label/tab-label-wrapper.directive.ts | 2 +- terminus-ui/tabs/src/tab/tab.component.ts | 20 +- .../tabs/testing/src/test-components.ts | 131 +++-- terminus-ui/toggle/src/toggle.component.ts | 7 +- terminus-ui/tooltip/src/tooltip.component.ts | 3 +- terminus-ui/tsconfig.json | 10 +- terminus-ui/typings.d.ts | 2 +- .../cva-provider-factory.spec.ts | 2 +- .../cva-provider-factory.ts | 2 +- .../input-has-changed/input-has-changed.ts | 6 +- terminus-ui/utilities/src/merge/merge.ts | 4 +- .../reactive-form-base.component.spec.ts | 2 +- .../reactive-form-base.component.ts | 13 +- .../strip-control-characters.ts | 9 +- .../src/type-coercion/is-abstract-control.ts | 4 +- .../src/type-coercion/is-drag-event.ts | 6 +- .../type-coercion/is-html-input-element.ts | 6 +- .../type-coercion/is-keyboard-event.spec.ts | 23 + .../src/type-coercion/is-keyboard-event.ts | 13 + .../src/type-coercion/is-mouse-event.ts | 13 + terminus-ui/utilities/src/utilities.module.ts | 2 + .../utilities/src/version/version.spec.ts | 5 +- terminus-ui/utilities/src/version/version.ts | 1 + .../src/validation-messages.component.spec.ts | 2 +- .../src/validation-messages.component.ts | 2 + .../src/validation-messages.module.ts | 5 +- .../src/validation-messages.service.ts | 8 +- .../src/validation-messages.services.spec.ts | 20 +- .../testing/src/public-api.ts | 1 + .../src/validation-messages.service.mock.ts | 2 +- .../validators/src/validators.service.spec.ts | 4 +- .../validators/src/validators.service.ts | 30 +- .../src/validators/greaterThan/greaterThan.ts | 6 +- .../inCollection/inCollection.spec.ts | 2 +- .../validators/inCollection/inCollection.ts | 6 +- .../src/validators/isInRange/isInRange.ts | 19 +- .../src/validators/lessThan/lessThan.ts | 4 +- .../src/validators/maxDate/maxDate.spec.ts | 8 +- .../src/validators/maxDate/maxDate.ts | 15 +- .../src/validators/minDate/minDate.spec.ts | 8 +- .../src/validators/minDate/minDate.ts | 18 +- .../testing/src/validators.service.mock.ts | 285 +++++------ tsconfig.json | 10 +- tslint.ci.json | 33 ++ tslint.json | 187 +------ tslint.spec.json | 22 + yarn.lock | 392 +++++++++++++-- 208 files changed, 4050 insertions(+), 2628 deletions(-) delete mode 100644 .eslintrc create mode 100644 .eslintrc.ci.js create mode 100644 .eslintrc.js create mode 100644 demo/app/components/spacing/spacing-styles.component.html create mode 100644 demo/app/components/spacing/spacing.component.scss create mode 100644 demo/tslint.json create mode 100644 terminus-ui/chart/src/chart-type-check.ts create mode 100644 terminus-ui/loading-overlay/src/loading-overlay.component.html create mode 100644 terminus-ui/table/src/table.component.html create mode 100644 terminus-ui/utilities/src/type-coercion/is-keyboard-event.spec.ts create mode 100644 terminus-ui/utilities/src/type-coercion/is-keyboard-event.ts create mode 100644 terminus-ui/utilities/src/type-coercion/is-mouse-event.ts rename terminus-ui/validation-messages/{ => testing}/src/validation-messages.service.mock.ts (68%) create mode 100644 tslint.ci.json create mode 100644 tslint.spec.json diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index 23b8d3f96..000000000 --- a/.eslintrc +++ /dev/null @@ -1,3 +0,0 @@ -"parserOptions": { - "ecmaVersion": 6 - } diff --git a/.eslintrc.ci.js b/.eslintrc.ci.js new file mode 100644 index 000000000..7239d48cd --- /dev/null +++ b/.eslintrc.ci.js @@ -0,0 +1,18 @@ +module.exports = { + "extends": ["@terminus/eslint-config-frontend"], + "parserOptions": { + "ecmaVersion": 6, + "project": "./tsconfig.json", + "sourceType": "module" + }, + "rules": { + "no-console": [ + "error", + { + "allow": [ + "warn" + ] + } + ] + } +} diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 000000000..9d0766ce2 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,18 @@ +module.exports = { + "extends": ["@terminus/eslint-config-frontend/development"], + "parserOptions": { + "ecmaVersion": 6, + "project": "./tsconfig.json", + "sourceType": "module" + }, + "rules": { + "no-console": [ + "error", + { + "allow": [ + "warn" + ] + } + ] + } +} diff --git a/demo/app/components/autocomplete/autocomplete.component.ts b/demo/app/components/autocomplete/autocomplete.component.ts index 68bebc3a6..062722ebd 100644 --- a/demo/app/components/autocomplete/autocomplete.component.ts +++ b/demo/app/components/autocomplete/autocomplete.component.ts @@ -14,16 +14,21 @@ import { of, Subscription, } from 'rxjs'; -import { delay, map, startWith, switchMap } from 'rxjs/operators'; +import { + delay, + map, + startWith, + switchMap, +} from 'rxjs/operators'; import { TsAutocompleteComparatorFn, TsAutocompleteComponent, } from '@terminus/ui/autocomplete'; -interface GitHubUser { - [key: string]: any; -} +// tslint:disable-next-line no-any +type GitHubUser = Record; + // Values used to seed initial selections const INITIAL: GitHubUser[] = [ @@ -96,6 +101,7 @@ const INJECTION_ITEM = { interface OptionType { id: string; login: string; + // tslint:disable-next-line no-any [key: string]: any; } @@ -128,12 +134,12 @@ export class AutocompleteComponent implements OnInit { inProgress = false; delayApiResponse = false; changesSubscription$!: Subscription; - users$: any; + users$: Observable | undefined; minCharacters = 4; ngOnInit() { - this.changesSubscription$ = this.auto.selection.subscribe((v: any) => { + this.changesSubscription$ = this.auto.selection.subscribe((v: OptionType) => { console.log('DEMO: subscription change ', v); }); @@ -148,7 +154,7 @@ export class AutocompleteComponent implements OnInit { return this.http.get(`https://api.github.com/search/users?q=${term}`) .pipe( delay(this.delayApiResponse ? 3000 : 0), - map((response: any) => { + map((response) => { this.inProgress = false; const items: GitHubUser[] = response['items']; @@ -193,9 +199,9 @@ export class AutocompleteComponent implements OnInit { } } - comparator: TsAutocompleteComparatorFn = (v: any) => v.id; + comparator: TsAutocompleteComparatorFn = (v: OptionType) => v.id; - displayFn(user?: any): string | undefined { + displayFn(user?: OptionType): string | undefined { return user ? user.login : undefined; } diff --git a/demo/app/components/chart/chart.component.ts b/demo/app/components/chart/chart.component.ts index 0cbf1cc74..7e1255ab6 100644 --- a/demo/app/components/chart/chart.component.ts +++ b/demo/app/components/chart/chart.component.ts @@ -2,22 +2,27 @@ import am4geodata_worldLow from '@amcharts/amcharts4-geodata/worldLow'; import * as am4charts from '@amcharts/amcharts4/charts'; import * as am4core from '@amcharts/amcharts4/core'; import * as am4maps from '@amcharts/amcharts4/maps'; +import { Component } from '@angular/core'; import { - AfterViewInit, - Component, -} from '@angular/core'; -import { + TsChart, + tsChartChordTypeCheck, TsChartComponent, + tsChartMapTypeCheck, + tsChartPieTypeCheck, + tsChartRadarTypeCheck, + tsChartSankeyTypeCheck, + tsChartTreeTypeCheck, TsChartVisualizationOptions, + tsChartXYTypeCheck } from '@terminus/ui/chart'; -const XY_DATA: {[key: string]: any}[] = []; +const XY_DATA: Record[] = []; let visits = 10; for (let i = 1; i < 366; i++) { visits += Math.round((Math.random() < 0.5 ? 1 : -1) * Math.random() * 10); - XY_DATA.push({ date: new Date(2018, 0, i), name: 'name' + i, value: visits }); + XY_DATA.push({ date: new Date(2018, 0, i), name: `name${i}`, value: visits }); } -const MAP_DATA: {[key: string]: any}[] = [{ +const MAP_DATA: Record[] = [{ latitude: 48.856614, longitude: 2.352222, title: 'Paris', @@ -38,40 +43,39 @@ const MAP_DATA: {[key: string]: any}[] = [{ selector: 'demo-chart', templateUrl: './chart.component.html', }) -export class ChartComponent implements AfterViewInit { +export class ChartComponent { public visualizationOptions: TsChartVisualizationOptions[] = [ 'xy', 'pie', 'map', 'radar', - 'treemap', + 'tree', 'sankey', 'chord', ]; visualization: TsChartVisualizationOptions = this.visualizationOptions[0]; - ngAfterViewInit() { - } - - chartCreated(chart) { + chartCreated(chart: TsChart) { this.setChartData(chart, this.visualization); } // Currently using `any` here as I'm not sure how to let the consumer know what type is returned - setChartData(chart: any, type: TsChartVisualizationOptions) { + setChartData(chart: TsChart, type: TsChartVisualizationOptions) { /** * XY */ - if (type === 'xy') { + if (tsChartXYTypeCheck(chart)) { chart.data = XY_DATA; const dateAxis = chart.xAxes.push(new am4charts.DateAxis()); dateAxis.renderer.grid.template.location = 0; const valueAxis = chart.yAxes.push(new am4charts.ValueAxis()); - valueAxis.tooltip.disabled = true; + if (valueAxis.tooltip) { + valueAxis.tooltip.disabled = true; + } valueAxis.renderer.minWidth = 35; const series = chart.series.push(new am4charts.LineSeries()); @@ -89,12 +93,9 @@ export class ChartComponent implements AfterViewInit { /** * MAP */ - if (type === 'map') { + if (tsChartMapTypeCheck(chart)) { const polygonSeries = new am4maps.MapPolygonSeries(); polygonSeries.useGeodata = true; - /* - *polygonSeries.exclude = ['AQ']; - */ polygonSeries.include = [ 'PT', 'ES', 'FR', 'DE', 'BE', 'NL', 'IT', 'AT', 'GB', 'IE', 'CH', 'LU', 'GF', 'SR', 'GY', 'VE', 'CO', 'EC', 'PE', 'BO', 'CL', 'AR', 'PY', 'UY', 'US', 'MX', 'CA', 'BR', 'PA', 'DR', 'HT', 'JM', 'CU', 'PA', 'CR', 'NI', 'HN', 'GT', 'MX', @@ -150,7 +151,7 @@ export class ChartComponent implements AfterViewInit { /** * PIE */ - if (type === 'pie') { + if (tsChartPieTypeCheck(chart)) { chart.data = [ { country: 'Lithuania', @@ -204,7 +205,7 @@ export class ChartComponent implements AfterViewInit { /** * RADAR */ - if (type === 'radar') { + if (tsChartRadarTypeCheck(chart)) { chart.data = [ { category: 'One', @@ -266,12 +267,13 @@ export class ChartComponent implements AfterViewInit { chart.padding(20, 20, 20, 20); - const categoryAxis = chart.xAxes.push(new am4charts.CategoryAxis()); + // NOTE: Not sure why the following `as any`s are needed. This code comes directly from AMCharts docs. + const categoryAxis = chart.xAxes.push(new am4charts.CategoryAxis() as any); categoryAxis.dataFields.category = 'category'; categoryAxis.renderer.labels.template.location = 0.5; categoryAxis.renderer.tooltipLocation = 0.5; - const valueAxis = chart.yAxes.push(new am4charts.ValueAxis()); + const valueAxis = chart.yAxes.push(new am4charts.ValueAxis() as any); valueAxis.tooltip.disabled = true; valueAxis.renderer.labels.template.horizontalCenter = 'left'; valueAxis.min = 0; @@ -314,7 +316,7 @@ export class ChartComponent implements AfterViewInit { chart.cursor = new am4charts.RadarCursor(); chart.cursor.xAxis = categoryAxis; - chart.cursor.fullWidthXLine = true; + chart.cursor.fullWidthLineX = true; chart.cursor.lineX.strokeOpacity = 0; chart.cursor.lineX.fillOpacity = 0.1; chart.cursor.lineX.fill = am4core.color('#000000'); @@ -323,7 +325,7 @@ export class ChartComponent implements AfterViewInit { /** * TREEMAP */ - if (type === 'treemap') { + if (tsChartTreeTypeCheck(chart)) { chart.data = [{ name: 'First', children: [ @@ -401,7 +403,7 @@ export class ChartComponent implements AfterViewInit { /** * SANKEY */ - if (type === 'sankey') { + if (tsChartSankeyTypeCheck(chart)) { // Set data chart.data = [ { from: 'A', to: 'D', value: 10, nodeColor: '#06D6A0' }, @@ -435,7 +437,7 @@ export class ChartComponent implements AfterViewInit { /** * CHORD */ - if (type === 'chord') { + if (tsChartChordTypeCheck(chart)) { chart.data = [ { from: 'A', to: 'D', value: 10, nodeColor: '#CDCDCD' }, { from: 'B', to: 'D', value: 8, nodeColor: '#06D6A0', linkColor: '#06D6A0', linkOpacity: 1 }, diff --git a/demo/app/components/file-upload/file-upload.component.ts b/demo/app/components/file-upload/file-upload.component.ts index 9aea0e059..b2719d859 100644 --- a/demo/app/components/file-upload/file-upload.component.ts +++ b/demo/app/components/file-upload/file-upload.component.ts @@ -173,7 +173,7 @@ export class FileUploadComponent { } - mimeTypeChange(change: TsSelectChange) { + mimeTypeChange(change: TsSelectChange) { if (change.value.length < 1) { this.mimeTypes = ['image/png', 'image/jpg', 'image/jpeg']; } diff --git a/demo/app/components/login-form/login-form.component.html b/demo/app/components/login-form/login-form.component.html index 6b00e501f..6acea3bb7 100644 --- a/demo/app/components/login-form/login-form.component.html +++ b/demo/app/components/login-form/login-form.component.html @@ -1,8 +1,13 @@ +
+ +
+
diff --git a/demo/app/components/login-form/login-form.component.ts b/demo/app/components/login-form/login-form.component.ts index 4f49b734f..717e8c32e 100644 --- a/demo/app/components/login-form/login-form.component.ts +++ b/demo/app/components/login-form/login-form.component.ts @@ -27,4 +27,15 @@ export class LoginFormComponent { }, 1000); } + resetForm() { + console.log('in demo reset'); + setTimeout(() => { + this.reset = true; + + setTimeout(() => { + this.reset = false; + }, 10); + }, 10); + } + } diff --git a/demo/app/components/navigation/navigation.component.ts b/demo/app/components/navigation/navigation.component.ts index 46472d6be..f6f81ba40 100644 --- a/demo/app/components/navigation/navigation.component.ts +++ b/demo/app/components/navigation/navigation.component.ts @@ -82,7 +82,7 @@ export class NavigationComponent { * * @param {Object} item The navigation item */ - triggerAction(payload: TsNavigationPayload): void { + public triggerAction(payload: TsNavigationPayload): void { console.log('DEMO: triggerAction: ', payload); if (payload.event.metaKey) { @@ -95,7 +95,7 @@ export class NavigationComponent { } - updateNav(): void { + public updateNav(): void { const newNav = NAV_ITEMS_MOCK.slice(0); newNav.unshift(NEW_NAV_ITEM); this.navigationItems$ = of(newNav); diff --git a/demo/app/components/paginator/paginator.component.html b/demo/app/components/paginator/paginator.component.html index 70d5ab471..00c2cf839 100644 --- a/demo/app/components/paginator/paginator.component.html +++ b/demo/app/components/paginator/paginator.component.html @@ -21,7 +21,7 @@

diff --git a/demo/app/components/paginator/paginator.component.ts b/demo/app/components/paginator/paginator.component.ts index 65ad8c882..2f3037167 100644 --- a/demo/app/components/paginator/paginator.component.ts +++ b/demo/app/components/paginator/paginator.component.ts @@ -1,5 +1,5 @@ import { - AfterViewInit, + ChangeDetectorRef, Component, ViewChild, } from '@angular/core'; @@ -14,26 +14,35 @@ import { TsStyleThemeTypes } from '@terminus/ui/utilities'; selector: 'demo-paginator', templateUrl: './paginator.component.html', }) -export class PaginatorComponent implements AfterViewInit { +export class PaginatorComponent { myTheme: TsStyleThemeTypes = 'primary'; recordCount = 114; showSelector = true; currentPageIndex = 0; location = 'below'; pages: number[] = [0, 1, 2, 3, 4, 5]; - zeroBased = false; + zeroBased = true; @ViewChild(TsPaginatorComponent) paginator!: TsPaginatorComponent; - ngAfterViewInit(): void { - setTimeout(() => { - this.pages = Array.apply(null, {length: this.paginator.pagesArray.length}).map(Number.call, Number); + constructor( + private changeDetectorRef: ChangeDetectorRef, + ) {} + + + updatePages(isZeroBased: boolean): void { + Promise.resolve().then(() => { + if (isZeroBased) { + this.pages = Array.from(Array(this.paginator.pagesArray.length).keys()); + } else { + this.pages = Array.from(Array(this.paginator.pagesArray.length).keys()).map(v => ++v); + } + this.changeDetectorRef.detectChanges(); }); } - onPageSelect(e: TsPaginatorMenuItem): void { console.log('DEMO: page selected: ', e); } diff --git a/demo/app/components/select/select.component.html b/demo/app/components/select/select.component.html index bb187006b..4ab832926 100644 --- a/demo/app/components/select/select.component.html +++ b/demo/app/components/select/select.component.html @@ -53,7 +53,7 @@

[formGroup]="myForm" novalidate fxLayout="column" - fxLayout.gt-sm="row" + fxLayout.gt-sm="row wrap" fxLayoutGap="1rem" > @@ -150,7 +150,7 @@

{{ option?.foo }}

-

+

Single Select w/Optgroups

diff --git a/demo/app/components/select/select.module.ts b/demo/app/components/select/select.module.ts index 23f78a1d0..05620e833 100644 --- a/demo/app/components/select/select.module.ts +++ b/demo/app/components/select/select.module.ts @@ -13,7 +13,17 @@ import { SelectComponent } from './select.component'; @NgModule({ // tslint:disable-next-line:max-line-length - imports: [CommonModule, SelectRoutingModule, FlexLayoutModule, FormsModule, ReactiveFormsModule, TsCardModule, TsSelectModule, TsSpacingModule, TsToggleModule], + imports: [ + CommonModule, + SelectRoutingModule, + FlexLayoutModule, + FormsModule, + ReactiveFormsModule, + TsCardModule, + TsSelectModule, + TsSpacingModule, + TsToggleModule, + ], declarations: [SelectComponent], }) export class SelectModule {} diff --git a/demo/app/components/spacing/spacing-styles.component.html b/demo/app/components/spacing/spacing-styles.component.html new file mode 100644 index 000000000..8e096da96 --- /dev/null +++ b/demo/app/components/spacing/spacing-styles.component.html @@ -0,0 +1,48 @@ +

+ Spacing added as 'padding' to each outlined div. +

+ + +
+ padding: spacing(small, 2) +
+ +
+ padding: spacing(small, 1) +
+ +
+ padding: spacing(small) +
+ +
+ padding: spacing() +
+ +
+ padding: spacing(large) +
+ +
+ padding: spacing(large, 1) +
+ +
+ padding: spacing(large, 2) +
+ +
+ padding: spacing(large, 3) +
+ +
+ padding: spacing(large, 4) +
+ +
+ padding: spacing(large, 5) +
+ +
+ padding: spacing(large, 6) +
diff --git a/demo/app/components/spacing/spacing-styles.component.ts b/demo/app/components/spacing/spacing-styles.component.ts index 498d39bbe..121760074 100644 --- a/demo/app/components/spacing/spacing-styles.component.ts +++ b/demo/app/components/spacing/spacing-styles.component.ts @@ -4,56 +4,7 @@ import { Component } from '@angular/core'; @Component({ selector: 'demo-spacing-styles', styleUrls: ['./spacing-styles.component.scss'], - template: ` -

- Spacing added as 'padding' to each outlined div. -

- - -
- padding: spacing(small, 2) -
- -
- padding: spacing(small, 1) -
- -
- padding: spacing(small) -
- -
- padding: spacing() -
- -
- padding: spacing(large) -
- -
- padding: spacing(large, 1) -
- -
- padding: spacing(large, 2) -
- -
- padding: spacing(large, 3) -
- -
- padding: spacing(large, 4) -
- -
- padding: spacing(large, 5) -
- -
- padding: spacing(large, 6) -
- `, + templateUrl: './spacing-styles.component.html', }) export class SpacingStylesComponent { } diff --git a/demo/app/components/spacing/spacing.component.scss b/demo/app/components/spacing/spacing.component.scss new file mode 100644 index 000000000..dcbcda539 --- /dev/null +++ b/demo/app/components/spacing/spacing.component.scss @@ -0,0 +1,4 @@ +div { + outline: 1px solid lightblue; + padding: 8px; +} diff --git a/demo/app/components/spacing/spacing.component.ts b/demo/app/components/spacing/spacing.component.ts index 9d6373c5f..16a25bfaf 100644 --- a/demo/app/components/spacing/spacing.component.ts +++ b/demo/app/components/spacing/spacing.component.ts @@ -3,12 +3,7 @@ import { Component } from '@angular/core'; @Component({ selector: 'demo-spacing', - styles: [` - div { - outline: 1px solid lightblue; - padding: 8px; - } - `], + styleUrls: ['./spacing.component.scss'], templateUrl: './spacing.component.html', }) export class SpacingComponent { diff --git a/demo/tsconfig.app.json b/demo/tsconfig.app.json index acedfd7e2..384b1420e 100644 --- a/demo/tsconfig.app.json +++ b/demo/tsconfig.app.json @@ -1,10 +1,7 @@ { - "extends": "../tsconfig.json", - "angularCompilerOptions": { - "preserveWhitespaces": false - }, + "extends": "./../tsconfig.json", "compilerOptions": { - "outDir": "../out-tsc/app", + "outDir": "./../out-tsc/app", "module": "es2015", "types": [] }, diff --git a/demo/tslint.json b/demo/tslint.json new file mode 100644 index 000000000..172e040d0 --- /dev/null +++ b/demo/tslint.json @@ -0,0 +1,15 @@ +{ + "extends": "./../tslint.json", + "rules": { + "component-selector": [ + true, + "element", + "demo", + "kebab-case" + ], + "member-access": false, + "prefer-on-push-component-change-detection": false, + "template-no-call-expression": false, + "template-use-track-by-function": false + } +} diff --git a/package.json b/package.json index bc3418902..f154ec8ae 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "url": "https://github.com/GetTerminus/terminus-ui/issues" }, "scripts": { - "//=> Section: Demo App": "==============================", + "//////// Section: Demo App": "==============================", "ng": "ng", "start:app": "yarn run build:styles && yarn run link && ng serve", "start:app:aot": "yarn run link && ng serve --aot", @@ -22,43 +22,49 @@ "pretest:app": "yarn run build && cpr dist/terminus-ui/helpers.scss node_modules/@terminus/ui/helpers.scss --overwrite && cpr dist/terminus-ui/terminus-ui.css node_modules/@terminus/ui/terminus-ui.css --overwrite", "test:app": "ng test", "lint:app": "yarn run lint:app:ts && yarn run lint:app:scss", - "lint:app:ts": "ng lint --fix", + "lint:app:ts": "npx tslint --project ./demo/tsconfig.app.json --config ./demo/tslint.json --format stylish", "lint:app:scss": "npx stylelint 'demo/**/*.scss' --fix", "e2e:app": "ng e2e", "link": "cd dist/terminus-ui && yarn link && cd ../../ && yarn link @terminus/ui", "unlink": "yarn unlink @terminus/ui && cd dist/terminus-ui && yarn unlink", - "//=> Section: Library:Build": "==============================", + "////////// Section: Library:Build": "==============================", "build": "yarn run build:ts && yarn run build:styles && yarn run build:files", "build:ts": "rimraf dist && ng-packagr -p terminus-ui/package.json", "build:ts:watch": "yarn run build:styles && ng-packagr -p terminus-ui/package.json --watch", "build:styles": "npx gulp generate:styles --gulpfile tooling/gulpfile.js", "build:files": "cpr README.md dist/terminus-ui/ && cpr LICENSE dist/terminus-ui/", "build:explore": "source-map-explorer dist/terminus-ui/bundles/terminus-ui.umd.min.js dist/terminus-ui/bundles/terminus-ui.umd.min.js.map", - "//=> Section: Library:Testing": "==============================", + "////////// Section: Library:Testing": "==============================", "test:NOTE": "jest --watch: currently hangs when determining which tests to run. For now we simply run all", "test": "jest --watch", "test:ci": "jest --runInBand --coverage", "test:ci:local": "jest --coverage", "test:debug": "jest --debug --runInBand", - "//=> Section: Documentation": "==============================", + "////////// Section: Documentation": "==============================", "predocs": "yarn run docs:toc", "docs": "yarn run docs:ts", "docs:ts": "npx compodoc -p terminus-ui/tsconfig.compodoc.json -d docs --hideGenerator --toggleMenuItems all --theme material", "docs:toc": "npx doctoc --title '**Table of Contents**' --maxlevel 4 .", "//=> Section: Yarn Management": "==============================", "fresh-yarn-install": "rm -rf node_modules && yarn install", - "//=> Section: Linting": "==============================", - "lint:ts": "npx tslint --fix --project ./terminus-ui/tsconfig.lint.json", - "lint:ts-spec": "npx tslint --fix --project ./terminus-ui/tsconfig.spec.json", - "lint:scss": "npx stylelint 'terminus-ui/**/*.scss' --fix", - "lint:scss:ci": "npx stylelint 'terminus-ui/**/*.scss'", - "lint": "yarn run lint:ts && yarn run lint:scss", - "lint:ci": "npx tslint --project ./terminus-ui/tsconfig.lint.json && npx stylelint './terminus-ui/**/*.scss'", + "////////// Section: Linting": "==============================", + "lint:tslint": "npx tslint --project ./terminus-ui/tsconfig.json --config ./tslint.json --format stylish", + "lint:tslint:fix": "npx tslint --project ./terminus-ui/tsconfig.json --config ./tslint.json --format stylish --fix", + "lint:tslint:spec": "npx tslint --project ./terminus-ui/tsconfig.spec.json --config ./tslint.spec.json --format stylish", + "lint:tslint:spec:fix": "npx tslint --project ./terminus-ui/tsconfig.spec.json --config ./tslint.spec.json --format stylish --fix", + "lint:tslint:ci": "npx tslint --project ./terminus-ui/tsconfig.json --config ./tslint.ci.json --format stylish", + "lint:eslint": "npx eslint \"terminus-ui/**/*.{js,ts}\" --ignore-pattern \"terminus-ui/**/*.d.ts\" --config .eslintrc.js", + "lint:eslint:fix": "npx eslint \"terminus-ui/**/*.{js,ts}\" --ignore-pattern \"terminus-ui/**/*.d.ts\" --config .eslintrc.js --fix", + "lint:eslint:ci": "npx eslint \"terminus-ui/**/*.{js,ts}\" --ignore-pattern \"terminus-ui/**/*.d.ts\" --config .eslintrc.ci.js", + "lint:scss": "npx stylelint 'terminus-ui/**/*.scss'", + "lint:scss:fix": "npx stylelint 'terminus-ui/**/*.scss' --fix", + "lint": "yarn run lint:tslint:fix && yarn run lint:eslint:fix && yarn run lint:scss:fix", + "lint:ci": "yarn run lint:tslint:ci && yarn run lint:eslint:ci && yarn run lint:scss", "codecov:upload": "npx codecov -f coverage/*.json", - "//=> Section: Release": "==============================", + "////////// Section: Release": "==============================", "semantic-release": "semantic-release", "check:next-release": "npx semantic-release --no-ci --dry-run", - "//=> Section: Tooling": "==============================", + "////////// Section: Tooling": "==============================", "lint-staged": "lint-staged", "validate:circleci": "circleci config validate -c .circleci/config.yml", "cm": "npx git-cz", @@ -84,11 +90,12 @@ }, "lint-staged": { "terminus-ui/**/*.spec.ts": [ - "yarn run lint:ts-spec", + "yarn run lint:tslint:spec:fix", "git add" ], "terminus-ui/**/!(*.spec|*.mock).ts": [ - "yarn run lint:ts", + "yarn run lint:tslint:fix", + "yarn run lint:eslint:fix", "git add" ], "terminus-ui/**/*.scss": [ @@ -173,7 +180,7 @@ "@angular/platform-browser": "^7.2.2", "@angular/platform-browser-dynamic": "^7.2.2", "@angular/router": "^7.2.2", - "@terminus/ngx-tools": "^6.5.1", + "@terminus/ngx-tools": "^6.6.1", "@terminus/ui": "latest", "date-fns": "2.0.0-alpha.26", "ngx-perfect-scrollbar": "^7.2.0", @@ -194,13 +201,14 @@ "@semantic-release/github": "^5.2.10", "@semantic-release/npm": "^5.1.4", "@semantic-release/release-notes-generator": "^7.1.4", + "@terminus/eslint-config-frontend": "^1.0.2", + "@terminus/tslint-config-frontend": "^1.0.3", "@types/jest": "^23.3.13", "@types/node": "^10.12.19", "all-contributors-cli": "^5.11.0", "autoprefixer": "^9.4.7", "camelcase": "^5.0.0", "code-notes": "^1.0.4", - "codelyzer": "^4.5.0", "commitizen": "^3.0.5", "condition-circle": "^2.0.2", "core-js": "^2.6.3", @@ -208,6 +216,7 @@ "cz-customizable": "^5.3.0", "del": "^3.0.0", "doctoc": "^1.4.0", + "eslint": "^5.16.0", "execa": "^1.0.0", "glob": "^7.1.3", "global": "^4.3.2", @@ -263,8 +272,8 @@ "systemjs": "^0.21.4", "tsickle": "^0.34.0", "tslib": "^1.9.3", - "tslint": "^5.12.1", - "typescript": "3.1.6", + "tslint": "^5.16.0", + "typescript": "~3.2.1", "validate-commit-msg": "^2.14.0", "weak": "^1.0.1", "zone.js": "^0.8.29" diff --git a/terminus-ui/autocomplete/src/autocomplete.component.spec.ts b/terminus-ui/autocomplete/src/autocomplete.component.spec.ts index 6c7653bfd..ad32cfef0 100644 --- a/terminus-ui/autocomplete/src/autocomplete.component.spec.ts +++ b/terminus-ui/autocomplete/src/autocomplete.component.spec.ts @@ -15,7 +15,9 @@ import { describe(`TsAutocompleteComponent`, function() { let component: TsAutocompleteComponent; - const opt1 = {id: 1}; + const opt1 = { + id: 1, + }; let trigger: jest.Mocked; beforeEach(() => { @@ -95,7 +97,9 @@ describe(`TsAutocompleteComponent`, function() { test(`should throw an error in dev mode when passed a value that is not a function`, () => { - expect(() => {component.displayWith = 3 as any; }) + expect(() => { + component.displayWith = 3 as any; + }) .toThrowError(`TsAutocompleteComponent: 'displayWith' must be passed a function.`); }); @@ -131,7 +135,9 @@ describe(`TsAutocompleteComponent`, function() { test(`should throw an error in dev mode when passed a value that is not a function`, () => { - expect(() => {component.multiple = 3 as any; }) + expect(() => { + component.multiple = 3 as any; + }) .toThrowError(`TsAutocompleteComponent: 'multiple' must be passed a 'TsAutocompleteComparatorFn' function.`); }); @@ -144,7 +150,11 @@ describe(`TsAutocompleteComponent`, function() { expect(component.selectedOptions.length).toEqual(0); component.selectionsControl = new FormControl(); - const initial = [{id: 1}, {id: 2}]; + const initial = [{ + id: 1, + }, { + id: 2, + }]; component.initialSelections = initial; expect(component.selectedOptions.length).toEqual(2); @@ -153,11 +163,17 @@ describe(`TsAutocompleteComponent`, function() { test(`should not mutate the original array`, () => { - const initial = [{id: 1}, {id: 2}]; + const initial = [{ + id: 1, + }, { + id: 2, + }]; component.initialSelections = initial; - component.multiple = (v) => v.id; + component.multiple = v => v.id; const event: any = createMockInstance(MatAutocompleteSelectedEvent); - const option = {id: 3}; + const option = { + id: 3, + }; event.option = { value: option, }; @@ -322,7 +338,9 @@ describe(`TsAutocompleteComponent`, function() { expect(component.selectedOptions).toEqual([opt1]); expect(component.selectionsControl.value).toEqual([opt1]); - const result = component.deselectOption({id: 2}); + const result = component.deselectOption({ + id: 2, + }); expect(result).toEqual(undefined); expect(component.selectedOptions).toEqual([opt1]); @@ -377,7 +395,7 @@ describe(`TsAutocompleteComponent`, function() { test(`should call resetSearch if there is no event.relatedTarget and in multiple selection mode`, () => { expect(component.selectionsControl.touched).toEqual(false); - component.multiple = (v) => v; + component.multiple = v => v; component.handleBlur(eventNoRelatedTarget); expect(component['resetSearch']).toHaveBeenCalled(); expect(component.selectionsControl.touched).toEqual(true); @@ -385,14 +403,14 @@ describe(`TsAutocompleteComponent`, function() { test(`should call resetSearch if the relatedTarget has no nodeName and is in multiple mode`, () => { - component.multiple = (v) => v; + component.multiple = v => v; component.handleBlur(eventNoNode); expect(component['resetSearch']).toHaveBeenCalled(); }); test(`should NOT call resetSearch if the nodeName is MAT-OPTION`, () => { - component.multiple = (v) => v; + component.multiple = v => v; component.handleBlur(eventOpt); expect(component['resetSearch']).not.toHaveBeenCalled(); }); @@ -400,7 +418,7 @@ describe(`TsAutocompleteComponent`, function() { test(`should call resetSearch if the nodeName isn't MAT-OPTION`, () => { - component.multiple = (v) => v; + component.multiple = v => v; component.handleBlur(eventDiv); expect(component['resetSearch']).toHaveBeenCalled(); }); @@ -501,9 +519,13 @@ describe(`TsAutocompleteComponent`, function() { component.ngAfterViewInit(); expect(component.selectedOptions).toEqual([]); - component.selectionsControl.setValue([{id: 9}]); + component.selectionsControl.setValue([{ + id: 9, + }]); - expect(component.selectedOptions).toEqual([{id: 9}]); + expect(component.selectedOptions).toEqual([{ + id: 9, + }]); }); diff --git a/terminus-ui/autocomplete/src/autocomplete.component.ts b/terminus-ui/autocomplete/src/autocomplete.component.ts index ea3c6234d..eda360228 100644 --- a/terminus-ui/autocomplete/src/autocomplete.component.ts +++ b/terminus-ui/autocomplete/src/autocomplete.component.ts @@ -1,3 +1,5 @@ +// NOTE: A method must be used to dynamically format values for the UI +// tslint:disable: template-no-call-expression import { AfterViewInit, ChangeDetectionStrategy, @@ -33,37 +35,34 @@ import { import { TS_SPACING } from '@terminus/ui/spacing'; import { TsStyleThemeTypes } from '@terminus/ui/utilities'; import { BehaviorSubject } from 'rxjs'; -import { debounceTime, distinctUntilChanged, filter } from 'rxjs/operators'; - - -export interface KeyboardEvent { - [key: string]: any; -} +import { + debounceTime, + distinctUntilChanged, + filter, +} from 'rxjs/operators'; -export interface MouseEvent { - [key: string]: any; -} /** * Define a type for allowed {@link TsAutocompleteComponent} formatter function */ -export type TsAutocompleteFormatterFn = (value: any) => string; +// tslint:disable-next-line no-any +export type TsAutocompleteFormatterFn = (value: OptionType) => string; /** * Define a type for allowed {@link TsAutocompleteComponent} comparator function */ -export type TsAutocompleteComparatorFn = (value: any) => string; - +export type TsAutocompleteComparatorFn = (value: OptionType) => string; export class TsAutocompleteSelectedEvent extends MatAutocompleteSelectedEvent {} +const DEFAULT_DEBOUNCE_MS = 200; +const DEFAULT_MINIMUM_CHARACTERS = 2; + /** * The autocomplete UI Component * - * @deprecated in favor of the new TsInputComponent. Target 11.x - * * #### QA CSS CLASSES * - `qa-autocomplete`: The primary container * - `qa-autocomplete-input`: The input element @@ -106,11 +105,12 @@ export class TsAutocompleteSelectedEvent extends MatAutocompleteSelectedEvent {} encapsulation: ViewEncapsulation.None, exportAs: 'tsAutocomplete', }) +// tslint:disable-next-line no-any export class TsAutocompleteComponent implements AfterViewInit, OnDestroy { /** * Define the flex gap spacing */ - flexGap = TS_SPACING.small[0]; + public flexGap = TS_SPACING.small[0]; /** * Management of the query string @@ -147,10 +147,10 @@ export class TsAutocompleteComponent impleme * Provide access to the input element */ @ViewChild('autocompleteTrigger') - set autocompleteTrigger(value: MatAutocompleteTrigger) { + public set autocompleteTrigger(value: MatAutocompleteTrigger) { this.trigger = value; } - get autocompleteTrigger(): MatAutocompleteTrigger { + public get autocompleteTrigger(): MatAutocompleteTrigger { return this.trigger; } private trigger!: MatAutocompleteTrigger; @@ -171,7 +171,7 @@ export class TsAutocompleteComponent impleme public get debounceDelay(): number { return this._debounceDelay; } - private _debounceDelay: number = 200; + private _debounceDelay = DEFAULT_DEBOUNCE_MS; /** * A function to output the UI text from the selected item @@ -179,28 +179,24 @@ export class TsAutocompleteComponent impleme * When undefined the full selection object will be used as the display value */ @Input() - public set displayWith(value: TsAutocompleteFormatterFn) { + public set displayWith(value: TsAutocompleteFormatterFn) { if (!value) { return; } if (isFunction(value)) { this.uiFormatFn = value; - } else { - // istanbul ignore else - if (isDevMode()) { - throw Error(`TsAutocompleteComponent: 'displayWith' must be passed a function.`); - } + } else if (isDevMode()) { + throw Error(`TsAutocompleteComponent: 'displayWith' must be passed a function.`); } } - public get displayWith(): TsAutocompleteFormatterFn { + public get displayWith(): TsAutocompleteFormatterFn { return this.uiFormatFn; } /** * Define a hint for the input */ - // FIXME: Fix potential overlap of hint and error messages @Input() public hint: string | undefined; @@ -220,30 +216,27 @@ export class TsAutocompleteComponent impleme public get minimumCharacters(): number { return this._minimumCharacters; } - private _minimumCharacters: number = 2; + private _minimumCharacters = DEFAULT_MINIMUM_CHARACTERS; /** * Define if multiple selections are allowed by passing in a comparator function */ @Input() - public set multiple(v: TsAutocompleteComparatorFn) { + public set multiple(v: TsAutocompleteComparatorFn) { if (!v) { return; } if (isFunction(v)) { this.comparatorFn = v; - } else { - // istanbul ignore else - if (isDevMode()) { - throw Error(`TsAutocompleteComponent: 'multiple' must be passed a 'TsAutocompleteComparatorFn' function.`); - } + } else if (isDevMode()) { + throw Error(`TsAutocompleteComponent: 'multiple' must be passed a 'TsAutocompleteComparatorFn' function.`); } } - public get multiple(): TsAutocompleteComparatorFn { + public get multiple(): TsAutocompleteComparatorFn { return this.comparatorFn; } - private comparatorFn!: TsAutocompleteComparatorFn; + private comparatorFn!: TsAutocompleteComparatorFn; /** * Define the name attribute value @@ -307,28 +300,28 @@ export class TsAutocompleteComponent impleme * Emit the selected chip */ @Output() - public optionSelected: EventEmitter = new EventEmitter(); + public readonly optionSelected: EventEmitter = new EventEmitter(); /** * Emit the removed chip */ @Output() - public optionRemoved: EventEmitter = new EventEmitter(); + public readonly optionRemoved: EventEmitter = new EventEmitter(); /** * Emit the current selection */ @Output() - public selection: EventEmitter = new EventEmitter(); + public readonly selection: EventEmitter = new EventEmitter(); /** * Emit the query string */ @Output() - public query: EventEmitter = new EventEmitter(); + public readonly query: EventEmitter = new EventEmitter(); - constructor( + public constructor( private changeDetectorRef: ChangeDetectorRef, ) {} @@ -344,7 +337,7 @@ export class TsAutocompleteComponent impleme // Take a stream of query changes this.querySubject.pipe( untilComponentDestroyed(this), - filter((v) => (typeof v === 'string') && v.length >= this.minimumCharacters), + filter(v => (typeof v === 'string') && v.length >= this.minimumCharacters), // Debounce the query changes debounceTime(this.debounceDelay), // Only allow a query through if it is different from the previous query @@ -464,22 +457,22 @@ export class TsAutocompleteComponent impleme */ public handleBlur(event: KeyboardEvent | MouseEvent): void { // NOTE(B$): cannot use dot syntax here since 'relatedTarget' doesn't exist on a KeyboardEvent - const eventValue: KeyboardEvent | MouseEvent | null = - (event && event['relatedTarget']) ? event['relatedTarget'] : null; + // eslint-disable-next-line dot-notation + const eventValue: Node | null = (event && event['relatedTarget']) ? event['relatedTarget'] : null; - if (eventValue && eventValue.nodeName && !!this.multiple) { - // If the blur event comes from the user clicking an option, `event.relatedTarget.nodeName` - // will be `MAT-OPTION`. - if (eventValue.nodeName !== 'MAT-OPTION') { - this.resetSearch(); - } - } else { - // If no eventValue exists, this was a blur event triggered by the Escape key - if (!!this.multiple) { + if (this.multiple) { + if (eventValue && eventValue.nodeName) { + // If the blur event comes from the user clicking an option, `event.relatedTarget.nodeName` will be `MAT-OPTION`. + if (eventValue.nodeName !== 'MAT-OPTION') { + this.resetSearch(); + } + } else { + // If no eventValue exists, this was a blur event triggered by the Escape key this.resetSearch(); } } + // Since the user never interacts directly with the 'selectionsControl' formControl, we need to // manually mark it as 'touched' to trigger validation messages. // istanbul ignore else @@ -514,7 +507,7 @@ export class TsAutocompleteComponent impleme * @param selection - The selected option * @param formatter - The UI formatter function */ - private setDuplicateError(control: FormControl, selection: OptionType, formatter?: TsAutocompleteFormatterFn): void { + private setDuplicateError(control: FormControl, selection: OptionType, formatter?: TsAutocompleteFormatterFn): void { const invalidResponse: ValidationErrors = { notUnique: { valid: false, diff --git a/terminus-ui/autofocus/src/autofocus.directive.ts b/terminus-ui/autofocus/src/autofocus.directive.ts index b77ddce64..9b73938c4 100644 --- a/terminus-ui/autofocus/src/autofocus.directive.ts +++ b/terminus-ui/autofocus/src/autofocus.directive.ts @@ -33,7 +33,7 @@ export class TsAutofocusDirective implements AfterViewInit { * Define if the element should be focused after initialization */ @Input() - public set tsAutofocus(value: any) { + public set tsAutofocus(value: string | boolean) { this.shouldFocus = coerceBooleanProperty(value); } @@ -53,11 +53,8 @@ export class TsAutofocusDirective implements AfterViewInit { if (el.focus) { el.focus(); this.changeDetectorRef.detectChanges(); - } else { - // istanbul ignore else - if (isDevMode()) { - throw Error(`TsAutofocusDirective must be used on an element that has a .focus() method.`); - } + } else if (isDevMode()) { + throw Error(`TsAutofocusDirective must be used on an element that has a .focus() method.`); } } } diff --git a/terminus-ui/button/src/button.component.spec.ts b/terminus-ui/button/src/button.component.spec.ts index e7f6f90db..0dbb8a606 100644 --- a/terminus-ui/button/src/button.component.spec.ts +++ b/terminus-ui/button/src/button.component.spec.ts @@ -1,11 +1,17 @@ -import { Component, ViewChild } from '@angular/core'; +import { + Component, + ViewChild, +} from '@angular/core'; import { By } from '@angular/platform-browser'; import { createComponent, createMouseEvent, } from '@terminus/ngx-tools/testing'; -import { ComponentFixture, tick } from '@angular/core/testing'; +import { + ComponentFixture, + tick, +} from '@angular/core/testing'; import { TsButtonComponent } from './button.component'; import { TsButtonModule } from './button.module'; @@ -44,336 +50,340 @@ class TestHostComponent implements OnInit, OnDestroy { describe(`TsButtonComponent`, function() { - let component: TestHostComponent; - let fixture: ComponentFixture; - let button: HTMLButtonElement; - let buttonComponent: TsButtonComponent; - - beforeEach(() => { - fixture = createComponent(TestHostComponent, [], [TsButtonModule]); - component = fixture.componentInstance; - buttonComponent = component.buttonComponent; - fixture.detectChanges(); - button = fixture.debugElement.query(By.css('.c-button')).nativeElement as HTMLButtonElement; - }); + let component: TestHostComponent; + let fixture: ComponentFixture; + let button: HTMLButtonElement; + let buttonComponent: TsButtonComponent; - describe(`isDisabled`, () => { + beforeEach(() => { + fixture = createComponent(TestHostComponent, [], [TsButtonModule]); + component = fixture.componentInstance; + buttonComponent = component.buttonComponent; + fixture.detectChanges(); + button = fixture.debugElement.query(By.css('.c-button')).nativeElement as HTMLButtonElement; + }); - test(`should not have button disabled`, () => { - component.disabled = false; - fixture.detectChanges(); - expect(buttonComponent.isDisabled).toEqual(false); - expect(button.disabled).toEqual(false); - button.click(); - expect(component.clicked).toHaveBeenCalled(); - }); + describe(`isDisabled`, () => { - test(`should have button disabled`, () => { - component.disabled = true; - fixture.detectChanges(); - expect(buttonComponent.isDisabled).toEqual(true); - expect(button.disabled).toEqual(true); - expect(component.clicked).not.toHaveBeenCalled(); - }); + test(`should not have button disabled`, () => { + component.disabled = false; + fixture.detectChanges(); + expect(buttonComponent.isDisabled).toEqual(false); + expect(button.disabled).toEqual(false); + button.click(); + expect(component.clicked).toHaveBeenCalled(); }); - test(`click`, () => { - component.buttonComponent.clicked.emit = jest.fn(); - button.click(); - expect(buttonComponent.clicked.emit).toHaveBeenCalled(); + test(`should have button disabled`, () => { + component.disabled = true; + fixture.detectChanges(); + expect(buttonComponent.isDisabled).toEqual(true); + expect(button.disabled).toEqual(true); + expect(component.clicked).not.toHaveBeenCalled(); }); + }); - describe(`showProgress`, () => { - test(`should set disabled attribute if showProgress is true`, () => { - component.showProgress = true; - fixture.detectChanges(); - expect(buttonComponent.showProgress).toEqual(true); - expect(button.getAttribute('disabled')).toEqual(''); - }); + test(`click`, () => { + component.buttonComponent.clicked.emit = jest.fn(); + button.click(); + expect(buttonComponent.clicked.emit).toHaveBeenCalled(); + }); - test(`should not set disabled if showProgress and disabled are false`, () => { - component.showProgress = false; - component.disabled = false; - fixture.detectChanges(); - expect(buttonComponent.showProgress).toEqual(false); - expect(button.getAttribute('disabled')).toEqual(null); - }); + describe(`showProgress`, () => { + test(`should set disabled attribute if showProgress is true`, () => { + component.showProgress = true; + fixture.detectChanges(); + expect(buttonComponent.showProgress).toEqual(true); + expect(button.getAttribute('disabled')).toEqual(''); }); - describe(`when collapsed is true`, function() { - test(`should have button collapsed class set`, function() { - component.collapsed = true; - fixture.detectChanges(); - expect(buttonComponent.isCollapsed).toEqual(true); - expect(button.classList).toContain('c-button--collapsed'); - }); + test(`should not set disabled if showProgress and disabled are false`, () => { + component.showProgress = false; + component.disabled = false; + fixture.detectChanges(); + expect(buttonComponent.showProgress).toEqual(false); + expect(button.getAttribute('disabled')).toEqual(null); + }); + }); + + describe(`when collapsed is true`, function() { + test(`should have button collapsed class set`, function() { + component.collapsed = true; + fixture.detectChanges(); + expect(buttonComponent.isCollapsed).toEqual(true); + expect(button.classList).toContain('c-button--collapsed'); }); + }); - describe(`when format === collapsable`, function() { + describe(`when format === collapsable`, function() { - test(`should set isCollapsed to false if a delay is set and the value is FALSE`, () => { - buttonComponent['collapseWithDelay'] = jest.fn(); - buttonComponent.collapseDelay = 400; - buttonComponent.collapsed = false; - fixture.detectChanges(); + test(`should set isCollapsed to false if a delay is set and the value is FALSE`, () => { + buttonComponent['collapseWithDelay'] = jest.fn(); + buttonComponent.collapseDelay = 400; + buttonComponent.collapsed = false; + fixture.detectChanges(); - expect(buttonComponent['collapseWithDelay']).toHaveBeenCalled(); - expect(buttonComponent.isCollapsed).toEqual(false); - expect(button.classList).not.toContain('c-button--collapsed'); - }); + expect(buttonComponent['collapseWithDelay']).toHaveBeenCalled(); + expect(buttonComponent.isCollapsed).toEqual(false); + expect(button.classList).not.toContain('c-button--collapsed'); + }); - test(`should not call collapseWithDelay if no delay is set and the value is FALSE`, () => { - component['collapseWithDelay'] = jest.fn(); - component.collapsed = false; + test(`should not call collapseWithDelay if no delay is set and the value is FALSE`, () => { + component['collapseWithDelay'] = jest.fn(); + component.collapsed = false; - expect(component['collapseWithDelay']).not.toHaveBeenCalled(); - expect(button.classList).not.toContain('c-button--collapsed'); - }); + expect(component['collapseWithDelay']).not.toHaveBeenCalled(); + expect(button.classList).not.toContain('c-button--collapsed'); + }); - test(`should not call collapseWithDelay if delay is set and the value is TRUE`, () => { - component['collapseWithDelay'] = jest.fn(); - component.collapseDelay = 400; - component.collapsed = true; + test(`should not call collapseWithDelay if delay is set and the value is TRUE`, () => { + component['collapseWithDelay'] = jest.fn(); + component.collapseDelay = 400; + component.collapsed = true; - expect(component['collapseWithDelay']).not.toHaveBeenCalled(); - expect(button.classList).not.toContain('c-button--collapsed'); - }); + expect(component['collapseWithDelay']).not.toHaveBeenCalled(); + expect(button.classList).not.toContain('c-button--collapsed'); }); + }); - describe(`when format !== collapsable`, () => { - - it(`should not call collapseWithDelay if the type is not collapsable`, () => { - component['collapseWithDelay'] = jest.fn(); - component.buttonComponent.format = 'filled'; - component.collapsed = false; + describe(`when format !== collapsable`, () => { - expect(component['collapseWithDelay']).not.toHaveBeenCalled(); - expect(button.classList).not.toContain('c-button--collapsed'); - }); + it(`should not call collapseWithDelay if the type is not collapsable`, () => { + component['collapseWithDelay'] = jest.fn(); + component.buttonComponent.format = 'filled'; + component.collapsed = false; + expect(component['collapseWithDelay']).not.toHaveBeenCalled(); + expect(button.classList).not.toContain('c-button--collapsed'); }); + }); - describe(`set format`, () => { - describe('when format === collapsable', () => { + describe(`set format`, () => { - it(`should set the collapseDelay to default if unset`, () => { - buttonComponent.format = 'collapsable'; + describe('when format === collapsable', () => { - expect(component.collapseDelay).toEqual(component['COLLAPSE_DEFAULT_DELAY']); - }); + it(`should set the collapseDelay to default if unset`, () => { + buttonComponent.format = 'collapsable'; + expect(component.collapseDelay).toEqual(component['COLLAPSE_DEFAULT_DELAY']); + }); - it(`should not set the collapseDelay to default if a value is passed in`, () => { - component.collapseDelay = 1000; - component.format = 'collapsable'; - fixture.detectChanges(); - expect(component.collapseDelay).toEqual(1000); - expect(button.classList).toContain('c-button--collapsable'); - }); + it(`should not set the collapseDelay to default if a value is passed in`, () => { + component.collapseDelay = 1000; + component.format = 'collapsable'; + fixture.detectChanges(); + expect(component.collapseDelay).toEqual(1000); + expect(button.classList).toContain('c-button--collapsable'); }); + }); - describe('when format !== collapsable', function() { - test(`should remove any existing collapseDelay`, () => { - buttonComponent.collapseDelay = 400; - buttonComponent.format = 'filled'; - fixture.detectChanges(); + describe('when format !== collapsable', function() { - expect(buttonComponent.collapseDelay).toBeUndefined(); - }); + test(`should remove any existing collapseDelay`, () => { + buttonComponent.collapseDelay = 400; + buttonComponent.format = 'filled'; + fixture.detectChanges(); + expect(buttonComponent.collapseDelay).toBeUndefined(); }); + }); - test(`should not update classes if no value is passed in`, () => { - component['updateClasses'] = jest.fn(); - component.format = null as any; - expect(component['updateClasses']).not.toHaveBeenCalled(); - expect(button.classList).toContain('c-button--filled'); - }); + test(`should not update classes if no value is passed in`, () => { + component['updateClasses'] = jest.fn(); + component.format = null as any; + expect(component['updateClasses']).not.toHaveBeenCalled(); + expect(button.classList).toContain('c-button--filled'); + }); - test(`should log a warning if an invalid value was passed in`, () => { - window.console.warn = jest.fn(); - buttonComponent['updateClasses'] = jest.fn(); - buttonComponent.format = 'foo' as any; - fixture.detectChanges(); - expect(window.console.warn).toHaveBeenCalled(); - expect(buttonComponent['updateClasses']).not.toHaveBeenCalled(); - }); + test(`should log a warning if an invalid value was passed in`, () => { + window.console.warn = jest.fn(); + buttonComponent['updateClasses'] = jest.fn(); + buttonComponent.format = 'foo' as any; + fixture.detectChanges(); - test(`should update classes if correct format is passed in`, () => { - component['updateClasses'] = jest.fn(); - component.format = 'filled' as any; + expect(window.console.warn).toHaveBeenCalled(); + expect(buttonComponent['updateClasses']).not.toHaveBeenCalled(); + }); - expect(component['updateClasses']).not.toHaveBeenCalled(); - expect(button.classList).toContain('c-button--filled'); - }); + test(`should update classes if correct format is passed in`, () => { + component['updateClasses'] = jest.fn(); + component.format = 'filled' as any; + expect(component['updateClasses']).not.toHaveBeenCalled(); + expect(button.classList).toContain('c-button--filled'); }); + }); - describe(`set theme`, () => { - test(`should not update classes if no value is passed in`, () => { - component['updateClasses'] = jest.fn(); - component.theme = null as any; + describe(`set theme`, () => { - expect(component['updateClasses']).not.toHaveBeenCalled(); - expect(button.classList).toContain('c-button--primary'); - expect(button.classList).not.toContain('c-button--accent'); - }); + test(`should not update classes if no value is passed in`, () => { + component['updateClasses'] = jest.fn(); + component.theme = null as any; + expect(component['updateClasses']).not.toHaveBeenCalled(); + expect(button.classList).toContain('c-button--primary'); + expect(button.classList).not.toContain('c-button--accent'); + }); - test(`should log a warning if an invalid value was passed in`, () => { - window.console.warn = jest.fn(); - buttonComponent['updateClasses'] = jest.fn(); - buttonComponent.theme = 'foo' as any; - fixture.detectChanges(); - expect(window.console.warn).toHaveBeenCalled(); - expect(buttonComponent['updateClasses']).not.toHaveBeenCalled(); - expect(button.classList).toContain('c-button--primary'); - expect(button.classList).not.toContain('c-button--accent'); + test(`should log a warning if an invalid value was passed in`, () => { + window.console.warn = jest.fn(); + buttonComponent['updateClasses'] = jest.fn(); + buttonComponent.theme = 'foo' as any; + fixture.detectChanges(); - }); + expect(window.console.warn).toHaveBeenCalled(); + expect(buttonComponent['updateClasses']).not.toHaveBeenCalled(); + expect(button.classList).toContain('c-button--primary'); + expect(button.classList).not.toContain('c-button--accent'); }); + }); - describe(`ngOnInit()`, function() { + describe(`ngOnInit()`, function() { - test(`should call collapseWithDelay if collapseDelay is set`, () => { - jest.useFakeTimers(); - component.format = 'collapsable'; - component.iconName = 'search'; - component.collapseDelay = 500; - fixture.detectChanges(); - buttonComponent.ngOnInit(); - jest.advanceTimersByTime(6000); - fixture.detectChanges(); - expect(button.classList).toContain('c-button--collapsed'); - jest.runAllTimers(); - }); + test(`should call collapseWithDelay if collapseDelay is set`, () => { + jest.useFakeTimers(); + component.format = 'collapsable'; + component.iconName = 'search'; + component.collapseDelay = 500; + fixture.detectChanges(); + buttonComponent.ngOnInit(); + jest.advanceTimersByTime(6000); + fixture.detectChanges(); + expect(button.classList).toContain('c-button--collapsed'); + jest.runAllTimers(); + }); - it(`should call not collapseWithDelay if collapseDelay is not set`, () => { - component['collapseWithDelay'] = jest.fn(); - component.collapseDelay = undefined; - component.ngOnInit(); - expect(component['collapseWithDelay']).not.toHaveBeenCalled(); - expect(button.classList).not.toContain('c-button--collapsable'); - }); + it(`should call not collapseWithDelay if collapseDelay is not set`, () => { + component['collapseWithDelay'] = jest.fn(); + component.collapseDelay = undefined; + component.ngOnInit(); + expect(component['collapseWithDelay']).not.toHaveBeenCalled(); + expect(button.classList).not.toContain('c-button--collapsable'); + }); - describe(`when format === collapsable`, () => { - beforeEach(() => { - buttonComponent.format = 'collapsable'; - buttonComponent['collapseWithDelay'] = jest.fn(); - buttonComponent.collapseDelay = 500; - }); + describe(`when format === collapsable`, () => { + beforeEach(() => { + buttonComponent.format = 'collapsable'; + buttonComponent['collapseWithDelay'] = jest.fn(); + buttonComponent.collapseDelay = 500; + }); - it(`should throw an error if the format is collapsable and no icon is set`, () => { - expect(() => {buttonComponent.ngOnInit(); }).toThrow(); - }); + it(`should throw an error if the format is collapsable and no icon is set`, () => { + expect(() => { + buttonComponent.ngOnInit(); + }).toThrow(); + }); - it(`should not throw an error if the format is collapsable and there is an icon set`, () => { - component.iconName = 'home'; - expect(() => {component.ngOnInit(); }).not.toThrow(); - expect(button.classList).not.toContain('c-button__icon'); - }); - }); + it(`should not throw an error if the format is collapsable and there is an icon set`, () => { + component.iconName = 'home'; + expect(() => { + component.ngOnInit(); + }).not.toThrow(); + expect(button.classList).not.toContain('c-button__icon'); + }); }); + }); - describe(`ngOnDestroy()`, () => { - beforeEach(() => { - buttonComponent.format = 'collapsable'; - buttonComponent.iconName = 'home'; - buttonComponent['changeDetectorRef'].detectChanges = jest.fn(); - buttonComponent['windowService'].nativeWindow.clearTimeout = jest.fn(); - buttonComponent['windowService'].nativeWindow.setTimeout = jest.fn().mockReturnValue(123); - }); + describe(`ngOnDestroy()`, () => { + beforeEach(() => { + buttonComponent.format = 'collapsable'; + buttonComponent.iconName = 'home'; + buttonComponent['changeDetectorRef'].detectChanges = jest.fn(); + buttonComponent['windowService'].nativeWindow.clearTimeout = jest.fn(); + buttonComponent['windowService'].nativeWindow.setTimeout = jest.fn().mockReturnValue(123); + }); - it(`should clear any existing timeouts`, () => { - buttonComponent.ngOnInit(); - expect(buttonComponent['collapseTimeoutId']).toEqual(123); - buttonComponent.ngOnDestroy(); - expect(buttonComponent['windowService'].nativeWindow.clearTimeout).toHaveBeenCalledWith(123); - }); + it(`should clear any existing timeouts`, () => { + buttonComponent.ngOnInit(); + expect(buttonComponent['collapseTimeoutId']).toEqual(123); + buttonComponent.ngOnDestroy(); + expect(buttonComponent['windowService'].nativeWindow.clearTimeout).toHaveBeenCalledWith(123); }); + }); - describe(`clickedButton()`, () => { - let mouseEvent: MouseEvent; - beforeEach(() => { - buttonComponent.clicked.emit = jest.fn(); - mouseEvent = createMouseEvent('click'); - }); + describe(`clickedButton()`, () => { + let mouseEvent: MouseEvent; + beforeEach(() => { + buttonComponent.clicked.emit = jest.fn(); + mouseEvent = createMouseEvent('click'); + }); - test(`should emit the click when interceptClick is false`, () => { - buttonComponent.clickedButton(mouseEvent); - expect(buttonComponent.clicked.emit).toHaveBeenCalledWith(mouseEvent); - }); + test(`should emit the click when interceptClick is false`, () => { + buttonComponent.clickedButton(mouseEvent); + expect(buttonComponent.clicked.emit).toHaveBeenCalledWith(mouseEvent); + }); - test(`should not emit the click when interceptClick is true`, () => { - buttonComponent.interceptClick = true; - buttonComponent.clickedButton(mouseEvent); - expect(buttonComponent.clicked.emit).not.toHaveBeenCalledWith(); - expect(buttonComponent.originalClickEvent).toEqual(mouseEvent); - }); + test(`should not emit the click when interceptClick is true`, () => { + buttonComponent.interceptClick = true; + buttonComponent.clickedButton(mouseEvent); + expect(buttonComponent.clicked.emit).not.toHaveBeenCalledWith(); + expect(buttonComponent.originalClickEvent).toEqual(mouseEvent); }); + }); - describe(`collapseWithDelay()`, () => { - beforeEach(() => { - buttonComponent.format = 'collapsable'; - buttonComponent['windowService'].nativeWindow.setTimeout = window.setTimeout; - }); + describe(`collapseWithDelay()`, () => { - test(`should set isCollapsed and trigger change detection after the delay`, () => { - jest.useFakeTimers(); + beforeEach(() => { + buttonComponent.format = 'collapsable'; + buttonComponent['windowService'].nativeWindow.setTimeout = window.setTimeout; + }); - const DELAY = 100; - buttonComponent['collapseWithDelay'](DELAY); - jest.advanceTimersByTime(2000); - fixture.detectChanges(); + test(`should set isCollapsed and trigger change detection after the delay`, () => { + jest.useFakeTimers(); - expect(buttonComponent.isCollapsed).toEqual(true); - jest.runAllTimers(); - expect(button.classList).toContain('c-button--collapsable'); - }); + const DELAY = 100; + buttonComponent['collapseWithDelay'](DELAY); + jest.advanceTimersByTime(2000); + fixture.detectChanges(); + expect(buttonComponent.isCollapsed).toEqual(true); + jest.runAllTimers(); + expect(button.classList).toContain('c-button--collapsable'); }); + }); + }); diff --git a/terminus-ui/button/src/button.component.ts b/terminus-ui/button/src/button.component.ts index 483e26376..d60a6f673 100644 --- a/terminus-ui/button/src/button.component.ts +++ b/terminus-ui/button/src/button.component.ts @@ -52,6 +52,8 @@ export type TsButtonFormatTypes export const tsButtonFormatTypesArray = ['filled', 'hollow', 'collapsable']; +const DEFAULT_COLLAPSE_DELAY_MS = 4000; + /** * A presentational component to render a button @@ -88,11 +90,6 @@ export const tsButtonFormatTypesArray = ['filled', 'hollow', 'collapsable']; exportAs: 'tsButton', }) export class TsButtonComponent implements OnInit, OnDestroy { - /** - * Define the default delay for collapsable buttons - */ - private COLLAPSE_DEFAULT_DELAY = 4000; - /** * Store a reference to the timeout needed for collapsable buttons */ @@ -157,8 +154,7 @@ export class TsButtonComponent implements OnInit, OnDestroy { // Verify the value is allowed if (tsButtonFormatTypesArray.indexOf(value) < 0 && isDevMode()) { - console.warn(`TsButtonComponent: "${value}" is not an allowed format. ` + - `See TsButtonFormatTypes for available options.`); + console.warn(`TsButtonComponent: "${value}" is not an allowed format. See TsButtonFormatTypes for available options.`); return; } @@ -168,13 +164,11 @@ export class TsButtonComponent implements OnInit, OnDestroy { if (this._format === 'collapsable') { // Set the collapse delay if (!this.collapseDelay) { - this.collapseDelay = this.COLLAPSE_DEFAULT_DELAY; + this.collapseDelay = DEFAULT_COLLAPSE_DELAY_MS; } - } else { + } else if (this.collapseDelay) { // If the format is NOT collapsable, remove the delay - if (this.collapseDelay) { - this.collapseDelay = undefined; - } + this.collapseDelay = undefined; } this.changeDetectorRef.detectChanges(); @@ -207,7 +201,7 @@ export class TsButtonComponent implements OnInit, OnDestroy { * Define the tabindex for the button */ @Input() - public tabIndex: number = 0; + public tabIndex = 0; /** * Define the theme @@ -220,8 +214,8 @@ export class TsButtonComponent implements OnInit, OnDestroy { // Verify the value is allowed if (tsStyleThemeTypesArray.indexOf(value) < 0 && isDevMode()) { - console.warn(`TsButtonComponent: "${value}" is not an allowed theme. ` + - `See TsStyleThemeTypes for available options.`); + console.warn(`TsButtonComponent: "${value}" is not an allowed theme. ` + + `See TsStyleThemeTypes for available options.`); return; } @@ -237,7 +231,7 @@ export class TsButtonComponent implements OnInit, OnDestroy { * Pass the click event through to the parent */ @Output() - public clicked: EventEmitter = new EventEmitter(); + public readonly clicked: EventEmitter = new EventEmitter(); /** * Provide access to the inner button element @@ -298,12 +292,12 @@ export class TsButtonComponent implements OnInit, OnDestroy { * @param event - The MouseEvent */ public clickedButton(event: MouseEvent): void { - // Allow the click to propagate - if (!this.interceptClick) { - this.clicked.emit(event); - } else { + if (this.interceptClick) { // Save the original event but don't emit the originalClickEvent this.originalClickEvent = event; + } else { + // Allow the click to propagate + this.clicked.emit(event); } } @@ -342,10 +336,14 @@ export class TsButtonComponent implements OnInit, OnDestroy { const formatOptions = ['filled', 'hollow', 'collapsable']; const isTheme = themeOptions.indexOf(classname) >= 0; const isFormat = formatOptions.indexOf(classname) >= 0; - // This 'any' is needed since the `mat-raised-button` directive overwrites elementRef + // NOTE: Underscore dangle name controlled by Material + /* eslint-disable no-underscore-dangle */ + // NOTE: This 'any' is needed since the `mat-raised-button` directive overwrites elementRef + // tslint:disable-next-line no-any const buttonEl = (this.button as any)._elementRef.nativeElement; - const themeClasses = ['c-button--primary', 'c-button--accent', 'c-button--warn']; - const formatClasses = ['c-button--filled', 'c-button--hollow', 'c-button--collapsable']; + /* eslint-enable no-underscore-dangle */ + const themeClasses = themeOptions.map(theme => `c-button--${theme}`); + const formatClasses = formatOptions.map(format => `c-button--${format}`); // If dealing with a theme class // istanbul ignore else diff --git a/terminus-ui/card/src/card-title.directive.spec.ts b/terminus-ui/card/src/card-title.directive.spec.ts index 52df1eedb..7e4c87a76 100644 --- a/terminus-ui/card/src/card-title.directive.spec.ts +++ b/terminus-ui/card/src/card-title.directive.spec.ts @@ -1,6 +1,4 @@ -import { - Component, -} from '@angular/core'; +import { Component } from '@angular/core'; import { By } from '@angular/platform-browser'; import { createComponent } from '@terminus/ngx-tools/testing'; diff --git a/terminus-ui/card/src/card-title.directive.ts b/terminus-ui/card/src/card-title.directive.ts index 4e0b7933d..12912442c 100644 --- a/terminus-ui/card/src/card-title.directive.ts +++ b/terminus-ui/card/src/card-title.directive.ts @@ -24,13 +24,13 @@ export class TsCardTitleDirective { @Input() public set tsTitleAccentBorder(value: boolean) { if (!isBoolean(value) && value && isDevMode()) { - console.warn(`TsCardTitleDirective: "tsTitleAccentBorder" value is not a boolean. ` + - `String values of 'true' and 'false' will no longer be coerced to a true boolean with the next release.`); + console.warn(`TsCardTitleDirective: "tsTitleAccentBorder" value is not a boolean. ` + + `String values of 'true' and 'false' will no longer be coerced to a true boolean with the next release.`); } const setTitleAccBorder = coerceBooleanProperty(value); if (setTitleAccBorder) { - this.tsCardTitle = this.tsCardTitle + ' c-card__title-accent-border'; + this.tsCardTitle = `${this.tsCardTitle } c-card__title-accent-border`; } } @@ -44,7 +44,7 @@ export class TsCardTitleDirective { * Set the card title class */ @HostBinding('class') - tsCardTitle = 'c-card__title'; + public tsCardTitle = 'c-card__title'; /** * Verify the directive is nested within a {@link TsCardComponent} diff --git a/terminus-ui/card/src/card.component.spec.ts b/terminus-ui/card/src/card.component.spec.ts index 5f021e5d6..38c793e0a 100644 --- a/terminus-ui/card/src/card.component.spec.ts +++ b/terminus-ui/card/src/card.component.spec.ts @@ -1,11 +1,15 @@ -import { Component, ViewChild } from '@angular/core'; +import { + Component, ViewChild, +} from '@angular/core'; import { ComponentFixture } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { createComponent } from '@terminus/ngx-tools/testing'; import { TsStyleThemeTypes } from '../../utilities/src/public-api'; -import { TsCardBorderOptions, TsCardComponent } from './card.component'; +import { + TsCardBorderOptions, TsCardComponent, +} from './card.component'; import { TsCardModule } from './card.module'; @Component({ diff --git a/terminus-ui/card/src/card.component.ts b/terminus-ui/card/src/card.component.ts index 460a9d3a1..9621b4384 100644 --- a/terminus-ui/card/src/card.component.ts +++ b/terminus-ui/card/src/card.component.ts @@ -92,9 +92,11 @@ export class TsCardComponent { public set aspectRatio(value: TsAspectRatioTypes) { const x: number = parseInt(value.split(':')[0], 10); const y: number = parseInt(value.split(':')[1], 10); - const percentage: number = ((y / x) * 100); + const percentageMultiplier = 100; + const percentage: number = (y / x) * percentageMultiplier; + const percentageMaxLength = 2; - this.aspectRatioPadding = `${percentage.toFixed(2)}%`; + this.aspectRatioPadding = `${percentage.toFixed(percentageMaxLength)}%`; } /** @@ -177,7 +179,7 @@ export class TsCardComponent { * Getter to return a border class if the border is set */ public get borderClass(): string { - return (!this.border || this.border === 'none') ? '' : `c-card--border-${this.border}` ; + return (!this.border || this.border === 'none') ? '' : `c-card--border-${this.border}`; } diff --git a/terminus-ui/chart/src/amcharts.service.ts b/terminus-ui/chart/src/amcharts.service.ts index 2b325d40e..d4461a86b 100644 --- a/terminus-ui/chart/src/amcharts.service.ts +++ b/terminus-ui/chart/src/amcharts.service.ts @@ -5,12 +5,14 @@ import { } from '@angular/core'; +// tslint:disable no-any export interface TsAmChartsToken { core: any; charts: any; maps?: any; themes?: any[]; } +// tslint:enable no-any /** * Create an injection token that the consumer can override with their version of Highcharts diff --git a/terminus-ui/chart/src/chart-type-check.ts b/terminus-ui/chart/src/chart-type-check.ts new file mode 100644 index 000000000..cc68ef287 --- /dev/null +++ b/terminus-ui/chart/src/chart-type-check.ts @@ -0,0 +1,79 @@ +import * as am4charts from '@amcharts/amcharts4/charts'; +import * as am4maps from '@amcharts/amcharts4/maps'; + +import { + TsChart, + TsChartVisualizationOptions, +} from './chart.component'; + + + +/** + * Coerce the type to XYChart + * + * @param chart - The chart to check + * @return Boolean + */ +export function tsChartXYTypeCheck(chart: TsChart): chart is am4charts.XYChart { + return chart.className === 'XYChart'; +} + +/** + * Coerce the type to PieChart + * + * @param chart - The chart to check + * @return Boolean + */ +export function tsChartPieTypeCheck(chart: TsChart): chart is am4charts.PieChart { + return chart.className === 'PieChart'; +} + +/** + * Coerce the type to MapChart + * + * @param chart - The chart to check + * @return Boolean + */ +export function tsChartMapTypeCheck(chart: TsChart): chart is am4maps.MapChart { + return chart.className === 'MapChart'; +} + +/** + * Coerce the type to RadarChart + * + * @param chart - The chart to check + * @return Boolean + */ +export function tsChartRadarTypeCheck(chart: TsChart): chart is am4charts.RadarChart { + return chart.className === 'RadarChart'; +} + +/** + * Coerce the type to TreeMap + * + * @param chart - The chart to check + * @return Boolean + */ +export function tsChartTreeTypeCheck(chart: TsChart): chart is am4charts.TreeMap { + return chart.className === 'TreeMap'; +} + +/** + * Coerce the type to SankeyDiagram + * + * @param chart - The chart to check + * @return Boolean + */ +export function tsChartSankeyTypeCheck(chart: TsChart): chart is am4charts.SankeyDiagram { + return chart.className === 'SankeyDiagram'; +} + +/** + * Coerce the type to ChordDiagram + * + * @param chart - The chart to check + * @return Boolean + */ +export function tsChartChordTypeCheck(chart: TsChart): chart is am4charts.ChordDiagram { + return chart.className === 'ChordDiagram'; +} diff --git a/terminus-ui/chart/src/chart.component.spec.ts b/terminus-ui/chart/src/chart.component.spec.ts index 5ec9fd6b1..5f223223f 100644 --- a/terminus-ui/chart/src/chart.component.spec.ts +++ b/terminus-ui/chart/src/chart.component.spec.ts @@ -5,13 +5,24 @@ import { Type, ViewChild, } from '@angular/core'; -import { - ComponentFixture, -} from '@angular/core/testing'; +import { ComponentFixture } from '@angular/core/testing'; import { createComponent as createComponentInner } from '@terminus/ngx-tools/testing'; import { TsAmChartsService } from './amcharts.service'; -import { TsChartComponent, TsChartVisualizationOptions } from './chart.component'; +import { + tsChartChordTypeCheck, + tsChartMapTypeCheck, + tsChartPieTypeCheck, + tsChartRadarTypeCheck, + tsChartSankeyTypeCheck, + tsChartTreeTypeCheck, + tsChartXYTypeCheck, +} from './chart-type-check'; +import { + TsChart, + TsChartComponent, + TsChartVisualizationOptions, +} from './chart.component'; import { TsChartModule } from './chart.module'; @@ -27,12 +38,12 @@ describe(`ChartComponent`, function() { describe(`ngOnInit`, () => { - test(`should log error if the amCharts library wasn't passed in`, () => { - window.console.error = jest.fn(); + test(`should log a warning if the amCharts library wasn't passed in`, () => { + window.console.warn = jest.fn(); const fixture = createComponent(SimpleHost, []); fixture.detectChanges(); - expect(window.console.error).toHaveBeenCalledWith(expect.stringContaining('The amCharts library was not provided')); + expect(window.console.warn).toHaveBeenCalledWith(expect.stringContaining('The amCharts library was not provided')); }); @@ -81,27 +92,27 @@ describe(`ChartComponent`, function() { describe(`init`, () => { - test(`should log an error if no chart could be created`, () => { - window.console.error = jest.fn(); + test(`should log an warning if no chart could be created`, () => { + window.console.warn = jest.fn(); const fixture = createComponent(VisualizationsHost); fixture.detectChanges(); fixture.componentInstance.visualization = 'foo' as any; fixture.detectChanges(); - expect(window.console.error).toHaveBeenCalledWith(expect.stringContaining('is not a supported chart type')); + expect(window.console.warn).toHaveBeenCalledWith(expect.stringContaining('is not a supported chart type')); }); - const visualizationTests: TsChartVisualizationOptions[] = ['xy', 'pie', 'map', 'radar', 'treemap', 'sankey', 'chord']; + const visualizationTests: TsChartVisualizationOptions[] = ['xy', 'pie', 'map', 'radar', 'tree', 'sankey', 'chord']; for (const t of visualizationTests) { test(`should initialize for the ${t} visualization`, () => { - window.console.error = jest.fn(); + window.console.warn = jest.fn(); const fixture = createComponent(VisualizationsHost); fixture.componentInstance.visualization = t; fixture.detectChanges(); expect(fixture.componentInstance.component['amCharts'].core.create).toHaveBeenCalled(); - expect(window.console.error).not.toHaveBeenCalled(); + expect(window.console.warn).not.toHaveBeenCalled(); }); } @@ -109,6 +120,66 @@ describe(`ChartComponent`, function() { }); + // TODO: Test types once we implement a tool to do so + describe(`chart type coercion`, function() { + + test(`should validate xy chart`, function() { + const chart = { + className: 'XYChart', + } as TsChart; + expect(tsChartXYTypeCheck(chart)).toEqual(true); + }); + + + test(`should validate pie chart`, function() { + const chart = { + className: 'PieChart', + } as TsChart; + expect(tsChartPieTypeCheck(chart)).toEqual(true); + }); + + + test(`should validate map chart`, function() { + const chart = { + className: 'MapChart', + } as TsChart; + expect(tsChartMapTypeCheck(chart)).toEqual(true); + }); + + + test(`should validate radar chart`, function() { + const chart = { + className: 'RadarChart', + } as TsChart; + expect(tsChartRadarTypeCheck(chart)).toEqual(true); + }); + + + test(`should validate tree chart`, function() { + const chart = { + className: 'TreeMap', + } as TsChart; + expect(tsChartTreeTypeCheck(chart)).toEqual(true); + }); + + + test(`should validate sankey chart`, function() { + const chart = { + className: 'SankeyDiagram', + } as TsChart; + expect(tsChartSankeyTypeCheck(chart)).toEqual(true); + }); + + + test(`should validate chord chart`, function() { + const chart = { + className: 'ChordDiagram', + } as TsChart; + expect(tsChartChordTypeCheck(chart)).toEqual(true); + }); + + }); + }); @@ -119,17 +190,15 @@ describe(`ChartComponent`, function() { */ class AmChartsServiceMock { - get amCharts() { + public get amCharts() { return { core: { - create: jest.fn(() => { - return { - responsive: { - enabled: false, - }, - dispose: jest.fn(), - }; - }), + create: jest.fn(() => ({ + responsive: { + enabled: false, + }, + dispose: jest.fn(), + })), }, charts: { XYChart: {}, @@ -173,22 +242,42 @@ function createComponent(component: Type, providers: Provider[] = AM_CHART */ @Component({ - template: ``, -}) + template: ``, + }) class SimpleHost { @ViewChild(TsChartComponent) - component: TsChartComponent; -} + public component: TsChartComponent; + } -/** - * TEMPLATES - */ @Component({ - template: ``, -}) + template: ``, + }) class VisualizationsHost { - visualization: TsChartVisualizationOptions | undefined; + public visualization: TsChartVisualizationOptions | undefined; @ViewChild(TsChartComponent) - component: TsChartComponent; -} + public component: TsChartComponent; + } + + @Component({ + template: ` + + `, + }) +class TypeChecking { + /* + *public visualization!: TsChartVisualizationOptions; + */ + public chart!: TsChart; + + @ViewChild(TsChartComponent) + public component!: TsChartComponent; + + public chartCreated(chart: TsChart): void { + console.log('chartCreated: ', chart.className); + this.chart = chart; + } + } diff --git a/terminus-ui/chart/src/chart.component.ts b/terminus-ui/chart/src/chart.component.ts index f55edaad9..a80a61ac7 100644 --- a/terminus-ui/chart/src/chart.component.ts +++ b/terminus-ui/chart/src/chart.component.ts @@ -1,3 +1,5 @@ +import * as am4charts from '@amcharts/amcharts4/charts'; +import * as am4maps from '@amcharts/amcharts4/maps'; import { ChangeDetectionStrategy, Component, @@ -16,23 +18,40 @@ import { } from '@angular/core'; import { inputHasChanged } from '@terminus/ui/utilities'; -import { TsAmChartsService, TsAmChartsToken } from './amcharts.service'; +import { + TsAmChartsService, + TsAmChartsToken, +} from './amcharts.service'; /** * Define the supported chart visualizations */ export type TsChartVisualizationOptions - = 'pie' - | 'xy' + = 'xy' + | 'pie' | 'map' | 'radar' - | 'treemap' + | 'tree' | 'sankey' | 'chord' ; +/** + * Define possible chart types + */ +export type TsChart + = am4charts.XYChart + | am4charts.PieChart + | am4maps.MapChart + | am4charts.RadarChart + | am4charts.TreeMap + | am4charts.SankeyDiagram + | am4charts.ChordDiagram +; + + /** * This is the chart UI Component * @@ -65,7 +84,8 @@ export class TsChartComponent implements OnInit, OnChanges, OnDestroy { /** * Store the initialized chart */ - public chart: any; + // tslint:disable-next-line no-any + public chart: TsChart | undefined; /** * Save a reference to the underlying amCharts library @@ -100,7 +120,7 @@ export class TsChartComponent implements OnInit, OnChanges, OnDestroy { * Emit an event containing the chart each time it is initialized */ @Output() - public chartInitialized: EventEmitter = new EventEmitter(); + public readonly chartInitialized: EventEmitter = new EventEmitter(); constructor( @@ -118,14 +138,10 @@ export class TsChartComponent implements OnInit, OnChanges, OnDestroy { // Don't initialize a chart if the Highcharts library wasn't passed in. if (this.amCharts) { this.init(this.visualization); - // istanbul ignore else - } else { - // istanbul ignore else - if (isDevMode()) { - console.error( - 'TsChartComponent: The amCharts library was not provided via injection token!', - ); - } + } else if (isDevMode()) { + console.warn( + 'TsChartComponent: The amCharts library was not provided via injection token!', + ); } } @@ -169,21 +185,14 @@ export class TsChartComponent implements OnInit, OnChanges, OnDestroy { private init(type: TsChartVisualizationOptions): void { this.zone.runOutsideAngular(() => { // Create the appropriate chart using a chained ternary - const chart: any = - type === 'xy' - ? this.amCharts.core.create(this.chartDiv.nativeElement, this.amCharts.charts.XYChart) - : type === 'pie' - ? this.amCharts.core.create(this.chartDiv.nativeElement, this.amCharts.charts.PieChart) - : type === 'map' - ? this.amCharts.core.create(this.chartDiv.nativeElement, this.amCharts.maps.MapChart) - : type === 'radar' - ? this.amCharts.core.create(this.chartDiv.nativeElement, this.amCharts.charts.RadarChart) - : type === 'treemap' - ? this.amCharts.core.create(this.chartDiv.nativeElement, this.amCharts.charts.TreeMap) - : type === 'sankey' - ? this.amCharts.core.create(this.chartDiv.nativeElement, this.amCharts.charts.SankeyDiagram) - : type === 'chord' - ? this.amCharts.core.create(this.chartDiv.nativeElement, this.amCharts.charts.ChordDiagram) + const chart: TsChart + = type === 'xy' ? this.amCharts.core.create(this.chartDiv.nativeElement, this.amCharts.charts.XYChart) + : type === 'pie' ? this.amCharts.core.create(this.chartDiv.nativeElement, this.amCharts.charts.PieChart) + : type === 'map' ? this.amCharts.core.create(this.chartDiv.nativeElement, this.amCharts.maps.MapChart) + : type === 'radar' ? this.amCharts.core.create(this.chartDiv.nativeElement, this.amCharts.charts.RadarChart) + : type === 'tree' ? this.amCharts.core.create(this.chartDiv.nativeElement, this.amCharts.charts.TreeMap) + : type === 'sankey' ? this.amCharts.core.create(this.chartDiv.nativeElement, this.amCharts.charts.SankeyDiagram) + : type === 'chord' ? this.amCharts.core.create(this.chartDiv.nativeElement, this.amCharts.charts.ChordDiagram) : undefined ; @@ -192,7 +201,7 @@ export class TsChartComponent implements OnInit, OnChanges, OnDestroy { this.chart = chart; this.chartInitialized.emit(chart); } else { - console.error(`TsChartComponent: ${this.visualization} is not a supported chart type. See TsChartVisualizationOptions.`); + console.warn(`TsChartComponent: ${type} is not a supported chart type. See TsChartVisualizationOptions.`); } }); } diff --git a/terminus-ui/chart/src/chart.module.ts b/terminus-ui/chart/src/chart.module.ts index 3f441de38..65ab43847 100644 --- a/terminus-ui/chart/src/chart.module.ts +++ b/terminus-ui/chart/src/chart.module.ts @@ -1,11 +1,14 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; -import { TS_AMCHARTS_TOKEN, TsAmChartsService } from './amcharts.service'; +import { + TS_AMCHARTS_TOKEN, TsAmChartsService, +} from './amcharts.service'; import { TsChartComponent } from './chart.component'; export * from './chart.component'; export * from './amcharts.service'; +export * from './chart-type-check'; @NgModule({ diff --git a/terminus-ui/checkbox/src/checkbox.component.spec.ts b/terminus-ui/checkbox/src/checkbox.component.spec.ts index 2c45cfe43..4431bc4a8 100644 --- a/terminus-ui/checkbox/src/checkbox.component.spec.ts +++ b/terminus-ui/checkbox/src/checkbox.component.spec.ts @@ -1,4 +1,7 @@ -import { Component, ViewChild } from '@angular/core'; +import { + Component, + ViewChild, +} from '@angular/core'; import { ComponentFixture, TestBed, diff --git a/terminus-ui/checkbox/src/checkbox.component.ts b/terminus-ui/checkbox/src/checkbox.component.ts index 090e9fdf5..72192dff7 100644 --- a/terminus-ui/checkbox/src/checkbox.component.ts +++ b/terminus-ui/checkbox/src/checkbox.component.ts @@ -58,10 +58,10 @@ let nextUniqueId = 0; templateUrl: './checkbox.component.html', styleUrls: ['./checkbox.component.scss'], host: { - class: 'ts-checkbox', + 'class': 'ts-checkbox', '[attr.id]': 'id', }, - providers: [ControlValueAccessorProviderFactory(TsCheckboxComponent)], + providers: [ControlValueAccessorProviderFactory(TsCheckboxComponent)], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, exportAs: 'tsCheckbox', @@ -76,7 +76,7 @@ export class TsCheckboxComponent extends TsReactiveFormBaseComponent { * Provide access to the MatCheckboxComponent */ @ViewChild(MatCheckbox) - checkbox!: MatCheckbox; + public checkbox!: MatCheckbox; /** * Define an ID for the component @@ -149,13 +149,13 @@ export class TsCheckboxComponent extends TsReactiveFormBaseComponent { * Emit an event on input change */ @Output() - readonly inputChange: EventEmitter = new EventEmitter(); + public readonly inputChange: EventEmitter = new EventEmitter(); /** * Emit a change when moving from the indeterminate state */ @Output() - readonly indeterminateChange: EventEmitter = new EventEmitter(); + public readonly indeterminateChange: EventEmitter = new EventEmitter(); constructor( diff --git a/terminus-ui/checkbox/testing/src/test-helpers.ts b/terminus-ui/checkbox/testing/src/test-helpers.ts index 084721f28..1251316c7 100644 --- a/terminus-ui/checkbox/testing/src/test-helpers.ts +++ b/terminus-ui/checkbox/testing/src/test-helpers.ts @@ -1,4 +1,6 @@ -import { DebugElement, Predicate } from '@angular/core'; +import { + DebugElement, Predicate, +} from '@angular/core'; import { ComponentFixture } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { TsCheckboxComponent } from '@terminus/ui/checkbox'; @@ -15,7 +17,7 @@ export function getAllCheckboxInstances(fixture: ComponentFixture): TsCheck if (!debugElements) { throw new Error(`'getAllCheckboxInstances' found no checkboxes`); } - return debugElements.map((i) => i.componentInstance); + return debugElements.map(i => i.componentInstance); } /** diff --git a/terminus-ui/confirmation/src/confirmation.directive.spec.ts b/terminus-ui/confirmation/src/confirmation.directive.spec.ts index e4b0c0aa1..44abe79a2 100644 --- a/terminus-ui/confirmation/src/confirmation.directive.spec.ts +++ b/terminus-ui/confirmation/src/confirmation.directive.spec.ts @@ -7,89 +7,37 @@ import { Output, ViewChild, } from '@angular/core'; +import { ComponentFixture } from '@angular/core/testing'; import { - ComponentFixture, -} from '@angular/core/testing'; -import { createComponent, expectNativeEl } from '@terminus/ngx-tools/testing'; + createComponent, + expectNativeEl, +} from '@terminus/ngx-tools/testing'; import { TsButtonModule } from '@terminus/ui'; import { TsConfirmationDirective } from './confirmation.directive'; import { TsConfirmationModule } from './confirmation.module'; - - -/******************************************* - * TsButtonComponentMock - *******************************************/ -// tslint:disable: component-class-suffix -@Component({ - selector: 'ts-button', - template: ` - - `, - host: { - class: 'ts-button', - }, - exportAs: 'tsButton', -}) -class TsButtonComponentMock { - public interceptClick: boolean = false; - public originalClickEvent!: MouseEvent; - @Input() - public showProgress = false; - @Output() - public clicked: EventEmitter = new EventEmitter(); - - public clickedButton(event: MouseEvent): void { - // Allow the click to propagate - if (!this.interceptClick) { - this.clicked.emit(event); - } else { - // Save the original event but don't emit the originalClickEvent - this.originalClickEvent = event; - } - } -} -// tslint:enable: component-class-suffix - - -/******************************************* - * TsButtonModuleMock - *******************************************/ -@NgModule({ - declarations: [ - TsButtonComponentMock, - ], - exports: [ - TsButtonComponentMock, - ], -}) -export class TsButtonModuleMock {} - - -/******************************************* +/** ***************************************** * TestHostComponent *******************************************/ @Component({ template: ` - - Foo - + >Foo `, }) class TestHostComponent { @ViewChild(TsConfirmationDirective) - directive!: TsConfirmationDirective; - confirmText; - cancelText; - explanation; + public directive!: TsConfirmationDirective; + public confirmText; + public cancelText; + public explanation; } @@ -242,7 +190,7 @@ describe(`TsConfirmationDirective`, function() { }); - describe(`Positioning` , () => { + describe(`Positioning`, () => { test(`should default to 'below'`, () => { jest.useFakeTimers(); button.click(); @@ -307,4 +255,3 @@ describe(`TsConfirmationDirective`, function() { }); - diff --git a/terminus-ui/confirmation/src/confirmation.directive.ts b/terminus-ui/confirmation/src/confirmation.directive.ts index 3765e6420..857b2cf77 100644 --- a/terminus-ui/confirmation/src/confirmation.directive.ts +++ b/terminus-ui/confirmation/src/confirmation.directive.ts @@ -1,5 +1,6 @@ import { ConnectedPositionStrategy, + FlexibleConnectedPositionStrategy, HorizontalConnectionPos, Overlay, OverlayConfig, @@ -22,7 +23,7 @@ import { import { untilComponentDestroyed } from '@terminus/ngx-tools'; import { coerceBooleanProperty } from '@terminus/ngx-tools/coercion'; import { TsButtonComponent } from '@terminus/ui/button'; -import { merge } from 'rxjs/operators'; +import { merge } from 'rxjs'; import { TsConfirmationOverlayComponent } from './confirmation-overlay.component'; @@ -134,8 +135,8 @@ export class TsConfirmationDirective implements OnDestroy, OnInit { @Input() public set overlayPosition(value: TsConfirmationOverlayPositionTypes) { if (value && isDevMode() && (allowedOverlayPositionTypes.indexOf(value) < 0)) { - console.warn(`TsConfirmationOverlay: "${value}" is not an allowed position.` + - `Allowed positions are defined by "allowedOverlayPositionTypes".`); + console.warn(`TsConfirmationOverlay: "${value}" is not an allowed position.` + + `Allowed positions are defined by "allowedOverlayPositionTypes".`); } this._overlayPosition = value; } @@ -149,7 +150,7 @@ export class TsConfirmationDirective implements OnDestroy, OnInit { * An event emitted when the confirmation is cancelled */ @Output() - public cancelled: EventEmitter = new EventEmitter(); + public readonly cancelled: EventEmitter = new EventEmitter(); constructor( @@ -208,9 +209,13 @@ export class TsConfirmationDirective implements OnDestroy, OnInit { // Create the overlay this.overlayRef = this.overlay.create(overlayConfig); - // Wire up listeners for overlay clicks - this.overlayRef._keydownEvents.pipe( - merge(this.overlayRef.backdropClick()), + // Wire up listeners for keydown events and overlay clicks + merge( + // NOTE: Naming controlled by the CDK + // eslint-disable-next-line no-underscore-dangle + this.overlayRef._keydownEvents, + this.overlayRef.backdropClick(), + ).pipe( untilComponentDestroyed(this), ).subscribe(() => { this.dismissOverlay(); @@ -242,23 +247,31 @@ export class TsConfirmationDirective implements OnDestroy, OnInit { /** * Configure the overlay */ - private generateOverlayConfig(value: TsConfirmationOverlayPositionTypes) { + private generateOverlayConfig(value: TsConfirmationOverlayPositionTypes = 'below') { let overlayPosOriginX: HorizontalConnectionPos = 'center'; let overlayPosOriginY: VerticalConnectionPos = 'bottom'; let overlayPosOverlayX: HorizontalConnectionPos = 'center'; let overlayPosOverlayY: VerticalConnectionPos = 'top'; let positionClass = 'ts-confirmation-overlay__panel-below'; + // Define custom offsets so that the full button is still visible after the overlay is opened + const OFFSET_Y = 16; + const OFFSET_X_BEFORE = -38; + const OFFSET_X_AFTER = 38; + let defaultOffsetY = 0; + let defaultOffsetX = 0; switch (value) { case ('above'): overlayPosOriginY = 'top'; overlayPosOverlayY = 'bottom'; positionClass = 'ts-confirmation-overlay__panel-above'; + defaultOffsetY = -(OFFSET_Y); break; case ('below'): overlayPosOriginY = 'bottom'; overlayPosOverlayY = 'top'; positionClass = 'ts-confirmation-overlay__panel-below'; + defaultOffsetY = OFFSET_Y; break; case ('before'): overlayPosOriginX = 'start'; @@ -266,6 +279,7 @@ export class TsConfirmationDirective implements OnDestroy, OnInit { overlayPosOverlayX = 'end'; overlayPosOverlayY = 'center'; positionClass = 'ts-confirmation-overlay__panel-before'; + defaultOffsetX = OFFSET_X_BEFORE; break; case ('after'): overlayPosOriginX = 'end'; @@ -273,25 +287,25 @@ export class TsConfirmationDirective implements OnDestroy, OnInit { overlayPosOverlayX = 'start'; overlayPosOverlayY = 'center'; positionClass = 'ts-confirmation-overlay__panel-after'; + defaultOffsetX = OFFSET_X_AFTER; break; - default: - overlayPosOriginY = 'bottom'; - overlayPosOverlayY = 'top'; - positionClass = 'ts-confirmation-overlay__panel-below'; - break; + // skip default - unreachable } - const positionStrategy: ConnectedPositionStrategy = this.overlay.position().connectedTo( - this.elementRef, - { - originX: overlayPosOriginX, - originY: overlayPosOriginY, - }, - { - overlayX: overlayPosOverlayX, - overlayY: overlayPosOverlayY, - }, - ); + + const positionStrategy: FlexibleConnectedPositionStrategy = + this.overlay.position() + .flexibleConnectedTo(this.elementRef) + .withDefaultOffsetY(defaultOffsetY) + .withDefaultOffsetX(defaultOffsetX) + .withPositions([ + { + originX: overlayPosOriginX, + originY: overlayPosOriginY, + overlayX: overlayPosOverlayX, + overlayY: overlayPosOverlayY, + }, + ]); const overlayConfig: OverlayConfig = new OverlayConfig({ positionStrategy, diff --git a/terminus-ui/copy/src/copy.component.spec.ts b/terminus-ui/copy/src/copy.component.spec.ts index fb6395f4c..b84b87edf 100644 --- a/terminus-ui/copy/src/copy.component.spec.ts +++ b/terminus-ui/copy/src/copy.component.spec.ts @@ -74,7 +74,9 @@ describe(`TsCopyComponent`, function() { addRange: noop, }); - component['document'].createRange = jest.fn().mockReturnValue({selectNodeContents: noop}); + component['document'].createRange = jest.fn().mockReturnValue({ + selectNodeContents: noop, + }); component.selectText(component.content.nativeElement, false, false); expect(component['window'].getSelection).toHaveBeenCalled(); diff --git a/terminus-ui/copy/src/copy.component.ts b/terminus-ui/copy/src/copy.component.ts index 8a859bd50..faad82c73 100644 --- a/terminus-ui/copy/src/copy.component.ts +++ b/terminus-ui/copy/src/copy.component.ts @@ -1,4 +1,5 @@ import { + ChangeDetectionStrategy, Component, ElementRef, Input, @@ -36,6 +37,7 @@ import { TsStyleThemeTypes } from '@terminus/ui/utilities'; host: { class: 'ts-copy', }, + changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, exportAs: 'tsCopy', }) @@ -53,13 +55,13 @@ export class TsCopyComponent { /** * Define the copy icon */ - public icon: string = 'content_copy'; + public icon = 'content_copy'; /** * Define the color of the material ripple */ // FIXME: This color should be coming from a config - public rippleColor: string = '#1a237e'; + public rippleColor = '#1a237e'; /** * Store a reference to the window object @@ -111,9 +113,9 @@ export class TsCopyComponent { if (hasInnerText) { return this.content.nativeElement.innerText; - } else { - return ''; } + return ''; + } @@ -136,7 +138,7 @@ export class TsCopyComponent { // NOTE: Adding the type of 'Range' to this causes an error with `range.selectNodeContents` // `Argument of type ElementRef is not assignable to type 'Node'` const range = this.document.createRange(); - + // tslint:disable-next-line no-any range.selectNodeContents(element as any); selection.removeAllRanges(); selection.addRange(range); diff --git a/terminus-ui/csv-entry/src/csv-entry.component.html b/terminus-ui/csv-entry/src/csv-entry.component.html index cf8ae9b8c..767586fb3 100644 --- a/terminus-ui/csv-entry/src/csv-entry.component.html +++ b/terminus-ui/csv-entry/src/csv-entry.component.html @@ -19,7 +19,7 @@ class="c-csv-entry__column-id" [class.c-csv-entry__column-id--invalid]="row.invalid" [attr.id]="'csv-row-id-' + (idIndex + 1)" - *ngFor="let row of rows?.controls; let idIndex = index;" + *ngFor="let row of rows?.controls; let idIndex = index" fxFlex="noshrink" fxLayout="row" fxLayoutAlign="center center" @@ -49,7 +49,7 @@ type="text" title="Header, Column: {{ getHeaderCellName(headerIndex) || headerIndex + 1 }}" [readonly]="(columnHeaders && columnHeaders[headerIndex])" - *ngFor="let c of headerCells?.controls; let headerIndex = index;" + *ngFor="let c of headerCells?.controls; let headerIndex = index" [attr.id]="createId(-1, headerIndex)" [formControlName]="headerIndex" (paste)="onPaste($event, true)" @@ -64,17 +64,18 @@
v.columns.join('\t')).join('\r\n') + '\r\n'; + const headers = `${content.headers.join('\t') }\r\n`; + const rows = `${content.records.map(v => v.columns.join('\t')).join('\r\n') }\r\n`; return headers + rows; } @@ -55,18 +59,18 @@ function stringifyForm(content: TsCSVFormContents): string { `, }) class TestHostComponent { - id: number | undefined; - maxRows: number | undefined; - columnCount: number | undefined; - rowCount: number | undefined; - columnValidators: undefined | (ValidatorFn | null)[]; - columnHeaders: undefined | string[]; - fullWidth: boolean; - outputFormat = 'csv'; - gotFile = jest.fn(); + public id: number | undefined; + public maxRows: number | undefined; + public columnCount: number | undefined; + public rowCount: number | undefined; + public columnValidators: undefined | (ValidatorFn | null)[]; + public columnHeaders: undefined | string[]; + public fullWidth: boolean; + public outputFormat = 'csv'; + public gotFile = jest.fn(); @ViewChild(TsCSVEntryComponent) - component!: TsCSVEntryComponent; + public component!: TsCSVEntryComponent; constructor( public validatorsService: TsValidatorsService, @@ -130,47 +134,110 @@ describe(`TsCSVEntryComponent`, function() { formContentTwoCol = { headers: ['foo', 'bar'], records: [ - { recordId: 0, columns: ['foo1', 'bar1'] }, - { recordId: 1, columns: ['foo2', 'bar2'] }, - { recordId: 2, columns: ['foo3', 'bar3'] }, + { + recordId: 0, + columns: ['foo1', 'bar1'], + }, + { + recordId: 1, + columns: ['foo2', 'bar2'], + }, + { + recordId: 2, + columns: ['foo3', 'bar3'], + }, ], }; formContentThreeCol = { headers: ['bing', 'bang', 'boom'], records: [ - { recordId: 0, columns: ['bing1', 'bang1', 'http://foo.com'] }, - { recordId: 1, columns: ['bing2', 'bang2', 'boom2'] }, - { recordId: 2, columns: ['bing3', 'bang3', 'boom3'] }, - { recordId: 3, columns: ['bing4', 'bang4', 'boom4'] }, + { + recordId: 0, + columns: ['bing1', 'bang1', 'http://foo.com'], + }, + { + recordId: 1, + columns: ['bing2', 'bang2', 'boom2'], + }, + { + recordId: 2, + columns: ['bing3', 'bang3', 'boom3'], + }, + { + recordId: 3, + columns: ['bing4', 'bang4', 'boom4'], + }, ], }; formContentManyErrors = { headers: ['bing', 'bang', 'boom'], records: [ - { recordId: 0, columns: ['bing1', 'http://foo.com', 'boom1'] }, - { recordId: 1, columns: ['bing2', 'bang2', 'boom2'] }, - { recordId: 2, columns: ['bing3', 'bang3', 'boom3'] }, - { recordId: 3, columns: ['bing4', 'bang4', 'boom4'] }, - { recordId: 4, columns: ['bing5', '1234567890987654321234567890', 'boom5'] }, - { recordId: 5, columns: ['bing6', 'bang6', 'boom6'] }, - { recordId: 6, columns: ['bing7', 'bang7', 'boom7'] }, - { recordId: 7, columns: ['bing8', 'bang8', 'boom8'] }, - { recordId: 8, columns: ['bing9', 'bang9', 'boom9'] }, + { + recordId: 0, + columns: ['bing1', 'http://foo.com', 'boom1'], + }, + { + recordId: 1, + columns: ['bing2', 'bang2', 'boom2'], + }, + { + recordId: 2, + columns: ['bing3', 'bang3', 'boom3'], + }, + { + recordId: 3, + columns: ['bing4', 'bang4', 'boom4'], + }, + { + recordId: 4, + columns: ['bing5', '1234567890987654321234567890', 'boom5'], + }, + { + recordId: 5, + columns: ['bing6', 'bang6', 'boom6'], + }, + { + recordId: 6, + columns: ['bing7', 'bang7', 'boom7'], + }, + { + recordId: 7, + columns: ['bing8', 'bang8', 'boom8'], + }, + { + recordId: 8, + columns: ['bing9', 'bang9', 'boom9'], + }, ], }; formContentRequiredErrors = { headers: ['bing', 'bang'], records: [ - { recordId: 0, columns: ['bing1', 'http://foo.com', 'boom1'] }, - { recordId: 1, columns: [null, 'bang2', 'boom2'] }, - { recordId: 2, columns: ['bing3', 'bang3', 'boom3'] }, + { + recordId: 0, + columns: ['bing1', 'http://foo.com', 'boom1'], + }, + { + recordId: 1, + columns: [null, 'bang2', 'boom2'], + }, + { + recordId: 2, + columns: ['bing3', 'bang3', 'boom3'], + }, ], }; formContentWithQuotesAndCommas = { headers: ['bing', 'bang'], records: [ - { recordId: 0, columns: ['a, b', '"foo"'] }, - { recordId: 1, columns: ['"foo, bar"', '"foo", "bar"'] }, + { + recordId: 0, + columns: ['a, b', '"foo"'], + }, + { + recordId: 1, + columns: ['"foo, bar"', '"foo", "bar"'], + }, ], }; /** @@ -179,32 +246,60 @@ describe(`TsCSVEntryComponent`, function() { TAB_EVENT = document.createEvent('KeyboardEvent'); TAB_EVENT.initEvent('keydown', true, false); Object.defineProperties(TAB_EVENT, { - code: { get: () => KEYS.TAB.code }, - key: { get: () => KEYS.TAB.code }, - keyCode: { get: () => KEYS.TAB.keyCode }, + code: { + get: () => KEYS.TAB.code, + }, + key: { + get: () => KEYS.TAB.code, + }, + keyCode: { + get: () => KEYS.TAB.keyCode, + }, }); SHIFT_TAB_EVENT = document.createEvent('KeyboardEvent'); SHIFT_TAB_EVENT.initEvent('keydown', true, false); Object.defineProperties(SHIFT_TAB_EVENT, { - code: { get: () => KEYS.TAB.code }, - key: { get: () => KEYS.TAB.code }, - keyCode: { get: () => KEYS.TAB.keyCode }, - shiftKey: { get: () => true }, + code: { + get: () => KEYS.TAB.code, + }, + key: { + get: () => KEYS.TAB.code, + }, + keyCode: { + get: () => KEYS.TAB.keyCode, + }, + shiftKey: { + get: () => true, + }, }); ENTER_EVENT = document.createEvent('KeyboardEvent'); ENTER_EVENT.initEvent('keydown', true, false); Object.defineProperties(ENTER_EVENT, { - code: { get: () => KEYS.ENTER.code }, - key: { get: () => KEYS.ENTER.code }, - keyCode: { get: () => KEYS.ENTER.keyCode }, + code: { + get: () => KEYS.ENTER.code, + }, + key: { + get: () => KEYS.ENTER.code, + }, + keyCode: { + get: () => KEYS.ENTER.keyCode, + }, }); SHIFT_ENTER_EVENT = document.createEvent('KeyboardEvent'); SHIFT_ENTER_EVENT.initEvent('keydown', true, false); Object.defineProperties(SHIFT_ENTER_EVENT, { - code: { get: () => KEYS.ENTER.code }, - key: { get: () => KEYS.ENTER.code }, - keyCode: { get: () => KEYS.ENTER.keyCode }, - shiftKey: { get: () => true }, + code: { + get: () => KEYS.ENTER.code, + }, + key: { + get: () => KEYS.ENTER.code, + }, + keyCode: { + get: () => KEYS.ENTER.keyCode, + }, + shiftKey: { + get: () => true, + }, }); /** @@ -582,7 +677,7 @@ describe(`TsCSVEntryComponent`, function() { }); - test(`should respect output format of tsv`, fakeAsync((done) => { + test(`should respect output format of tsv`, fakeAsync(done => { jest.useFakeTimers(); expect(firstHeaderCell.value).toEqual(''); firstHeaderCell.dispatchEvent(createPasteEvent(formContentTwoCol)); @@ -601,7 +696,7 @@ describe(`TsCSVEntryComponent`, function() { })); - test(`should respect output format of csv`, fakeAsync((done) => { + test(`should respect output format of csv`, fakeAsync(done => { jest.useFakeTimers(); expect(firstHeaderCell.value).toEqual(''); firstHeaderCell.dispatchEvent(createPasteEvent(formContentTwoCol)); @@ -657,6 +752,11 @@ describe(`TsCSVEntryComponent`, function() { }); + test(`should do nothing if the event has no target`, () => { + expect(component.onScroll({} as any)).toEqual(undefined); + }); + + test(`should prevent the default event when reaching the left edge`, () => { component.onScroll(event); expect(event.preventDefault).not.toHaveBeenCalled(); @@ -665,16 +765,6 @@ describe(`TsCSVEntryComponent`, function() { component.onScroll(event); expect(event.preventDefault).toHaveBeenCalled(); - - // Check that it works with srcElement instead - Object.defineProperties(event, { - target: { - value: null, - }, - }); - - component.onScroll(event); - expect(event.preventDefault).toHaveBeenCalledTimes(2); }); @@ -728,7 +818,8 @@ describe(`TsCSVEntryComponent`, function() { }); - test(`should correctly handle commas and quotes`, fakeAsync((done) => { + + test(`should correctly handle commas and quotes`, fakeAsync(done => { jest.useFakeTimers(); hostComponent.outputFormat = 'csv'; firstHeaderCell.dispatchEvent(createPasteEvent(formContentWithQuotesAndCommas)); @@ -747,4 +838,20 @@ describe(`TsCSVEntryComponent`, function() { reader.readAsText(content); })); + + describe(`collectErrors`, function() { + + test(`should return null if the form group is not found`, function() { + component.recordsForm = new FormGroup({}); + expect(component.collectErrors()).toEqual(null); + }); + + + test(`should return null if no errors exist`, function() { + component['getFormErrors'] = jest.fn(() => null); + expect(component.collectErrors()).toEqual(null); + }); + + }); + }); diff --git a/terminus-ui/csv-entry/src/csv-entry.component.ts b/terminus-ui/csv-entry/src/csv-entry.component.ts index cb5c4d38d..613e884e1 100644 --- a/terminus-ui/csv-entry/src/csv-entry.component.ts +++ b/terminus-ui/csv-entry/src/csv-entry.component.ts @@ -1,3 +1,7 @@ +// NOTE: Moving items like `getHeaderCellName` into the template would add too much logic to the template +// tslint:disable: template-no-call-expression +// NOTE: Using trackBy on form groups actually changes the output since there is no static ID to reference +// tslint:disable template-use-track-by-function import { ChangeDetectionStrategy, ChangeDetectorRef, @@ -10,6 +14,7 @@ import { ViewEncapsulation, } from '@angular/core'; import { + AbstractControl, FormArray, FormBuilder, FormControl, @@ -29,7 +34,6 @@ import { stripControlCharacters } from '@terminus/ui/utilities'; import { debounceTime } from 'rxjs/operators'; - /** * The structure for an individual row */ @@ -46,11 +50,47 @@ export interface TsCSVFormContents { records: TsCSVEntryRecord[]; } +/** + * The structure for a required error + */ +export interface TsCSVRequiredError { + rowId: number; + valid: boolean; +} + +/** + * The structure for a URL error + */ +export interface TsCSVUrlError { + actual: string; + rowId: number; + valid: boolean; +} + +/** + * The structure for the error set + */ +export interface TsCSVFormError { + // The control name + control: string; + required?: TsCSVRequiredError; + url?: TsCSVUrlError; +} + +export interface TsCSVRow { + recordId: FormControl; + columns: FormArray; +} + /** * Unique ID for each instance */ let nextUniqueId = 0; +const DEFAULT_ROW_COUNT = 4; +const DEFAULT_COLUMN_COUNT = 2; +const DEFAULT_MAX_ROWS = 2000; +const DEFAULT_VALIDATION_MESSAGES_MAX = 6; /** @@ -86,7 +126,7 @@ let nextUniqueId = 0; templateUrl: './csv-entry.component.html', styleUrls: ['./csv-entry.component.scss'], host: { - class: 'ts-csv-entry', + 'class': 'ts-csv-entry', '[class.c-csv-entry--full-width]': 'fullWidth', '[attr.id]': 'id', }, @@ -100,21 +140,6 @@ export class TsCSVEntryComponent implements OnInit, OnDestroy { */ protected _uid = `ts-csv-entry-${nextUniqueId++}`; - /** - * Define the default number of rows - */ - public defaultRowCount = 4; - - /** - * Define the default number of columns - */ - public defaultColumnCount = 2; - - /** - * Define the default for the maximum allowed rows - */ - public defaultMaxRows = 2000; - /** * Expose the flexbox layout gap */ @@ -125,11 +150,6 @@ export class TsCSVEntryComponent implements OnInit, OnDestroy { */ public tooManyRowsMessage: string | null = null; - /** - * Define the number of validation messages that can be shown at once - */ - public maximumValidationMessages = 6; - /** * Store records (rows) */ @@ -143,10 +163,10 @@ export class TsCSVEntryComponent implements OnInit, OnDestroy { records: this.formBuilder.array([]), }); - /* - *public validationMessages: string[] | undefined; + /** + * Store a reference to all existing errors */ - public allErrors: {[key: string]: any}[] | null = null; + public allErrors: TsCSVFormError[] | null = null; /** * Get header cells as a form array @@ -179,36 +199,36 @@ export class TsCSVEntryComponent implements OnInit, OnDestroy { */ @Input() public set maxRows(value: number) { - this._maxRows = coerceNumberProperty(value, this.defaultMaxRows); + this._maxRows = coerceNumberProperty(value, DEFAULT_MAX_ROWS); } public get maxRows(): number { return this._maxRows; } - private _maxRows: number = this.defaultMaxRows; + private _maxRows: number = DEFAULT_MAX_ROWS; /** * Set the number of columns */ @Input() public set columnCount(value: number) { - this._columnCount = coerceNumberProperty(value, this.defaultColumnCount); + this._columnCount = coerceNumberProperty(value, DEFAULT_COLUMN_COUNT); } public get columnCount(): number { return this._columnCount; } - private _columnCount: number = this.defaultColumnCount; + private _columnCount: number = DEFAULT_COLUMN_COUNT; /** * Define the number of rows */ @Input() public set rowCount(value: number) { - this._rowCount = coerceNumberProperty(value, this.defaultRowCount); + this._rowCount = coerceNumberProperty(value, DEFAULT_ROW_COUNT); } public get rowCount(): number { return this._rowCount; } - private _rowCount: number = this.defaultRowCount; + private _rowCount: number = DEFAULT_ROW_COUNT; /** * Allow full-width mode @@ -256,7 +276,7 @@ export class TsCSVEntryComponent implements OnInit, OnDestroy { * Emit the built file blob */ @Output() - public blobGenerated: EventEmitter = new EventEmitter(); + public readonly blobGenerated: EventEmitter = new EventEmitter(); constructor( @@ -277,7 +297,7 @@ export class TsCSVEntryComponent implements OnInit, OnDestroy { // Let the form values 'settle' before we emit anything debounceTime(1), untilComponentDestroyed(this), - ).subscribe((v) => { + ).subscribe(v => { const blob = this.generateBlob(v); this.blobGenerated.emit(blob); }); @@ -299,6 +319,7 @@ export class TsCSVEntryComponent implements OnInit, OnDestroy { */ public addRows(rowCount = 1, columnCount: number = this.columnCount, content?: string[][], index?: number): void { if ((this.rows.length + rowCount) > this.maxRows) { + // eslint-disable-next-line no-magic-numbers const rowsThatDontFit = (rowCount === 1 ? 2 : rowCount) - ((this.rows.length + rowCount) - this.maxRows); this.tooManyRowsMessage = `Adding ${rowsThatDontFit} row${rowsThatDontFit > 1 ? 's' : ''} would exceed the maximum rows allowed (${this.maxRows}).`; @@ -310,7 +331,7 @@ export class TsCSVEntryComponent implements OnInit, OnDestroy { } for (let i = 0; i < rowCount; i += 1) { - const indexToInjectAt: number = (index !== undefined ? index : this.rowCount) + i; + const indexToInjectAt: number = (index === undefined ? this.rowCount : index) + i; const c: string[] | null = content ? content[i] : null; const createdRow: FormGroup = this.createRow(this.rows.length, c); @@ -429,10 +450,17 @@ export class TsCSVEntryComponent implements OnInit, OnDestroy { return; } const dir: string = (event.deltaX < 0) ? 'right' : 'left'; - // TypeScript doesn't believe `form` exists on `EventTarget` - const targetEl: any = event.target || event.srcElement; + // NOTE: TypeScript doesn't believe `form` exists on `EventTarget` + // tslint:disable-next-line no-any + const targetEl: any = event.target; + + if (!targetEl) { + return; + } + const borderSize = 2; - const scrollRight: number = targetEl.form.scrollWidth - (targetEl.form.offsetWidth + borderSize) - targetEl.form.scrollLeft; + const scrollRight: number = + targetEl.form.scrollWidth - (parseInt(targetEl.form.offsetWidth, 10) + borderSize) - targetEl.form.scrollLeft; const scrollLeft = targetEl.form.scrollLeft; const stopRightScroll: boolean = (dir === 'right') && (scrollLeft < 1); const stopLeftScroll: boolean = (dir === 'left') && (scrollRight < 1); @@ -457,7 +485,7 @@ export class TsCSVEntryComponent implements OnInit, OnDestroy { const [rowId, columnId]: string[] = currentCellId.split('X'); const row: string = rowId.split('_')[1]; const column: string = columnId.split('_')[1]; - const newId: string = 'r_' + (parseInt(row, 10) + (up ? -1 : 1)) + 'Xc_' + column; + const newId = `r_${parseInt(row, 10) + (up ? -1 : 1)}Xc_${column}`; const input: HTMLElement | null = this.documentService.document.querySelector(`#${newId}`); if (input) { @@ -498,16 +526,14 @@ export class TsCSVEntryComponent implements OnInit, OnDestroy { } else { newColumnNumber = column - 1; } - } else { + } else if (islastColumn) { // Forward - if (islastColumn) { - newColumnNumber = 0; - newRowNumber += 1; - } else { - newColumnNumber = column + 1; - } + newColumnNumber = 0; + newRowNumber += 1; + } else { + newColumnNumber = column + 1; } - const newId: string = 'r_' + newRowNumber + 'Xc_' + newColumnNumber; + const newId = `r_${newRowNumber}Xc_${newColumnNumber}`; const input: HTMLElement | null = this.documentService.document.querySelector(`#${newId}`); // istanbul ignore else @@ -532,7 +558,7 @@ export class TsCSVEntryComponent implements OnInit, OnDestroy { /** * Collect all errors from the recordsForm and set to allErrors */ - public collectErrors(): {[key: string]: any}[] | null { + public collectErrors(): TsCSVFormError[] | null { const group: FormArray | null = this.recordsForm.get('records') as FormArray; // istanbul ignore else @@ -541,21 +567,19 @@ export class TsCSVEntryComponent implements OnInit, OnDestroy { // istanbul ignore else if (errors) { - const resultsArray: {[key: string]: any}[] = Object.keys(errors).map((key) => { - return { - control: key, - // De-duplicate the errors array - [key]: errors[key].filter((el, i, arr) => arr.indexOf(el) === i), - }; - }); + const resultsArray: TsCSVFormError[] = Object.keys(errors).map(key => ({ + control: key, + // De-duplicate the errors array + [key]: errors[key].filter((el, i, arr) => arr.indexOf(el) === i), + })); return resultsArray; - } else { - return null; } - } else { return null; + } + return null; + } @@ -569,7 +593,7 @@ export class TsCSVEntryComponent implements OnInit, OnDestroy { */ public get validationMessages(): string[] | undefined { if (!this.allErrors) { - return; + return undefined; } const messages: string[] = []; @@ -579,11 +603,12 @@ export class TsCSVEntryComponent implements OnInit, OnDestroy { for (const error of errorObj[name]) { let message = ''; // The ID is zero-based - message += `Row ${error.rowId + 1}: `; + message += `Row ${parseInt(error.rowId, 10) + 1}: `; // istanbul ignore else if (name === 'url') { const maxItemLength = 20; - const errorItem = (error.actual.length > maxItemLength) ? error.actual.slice(0, maxItemLength) + '...' : error.actual; + // tslint:disable-next-line + const errorItem = (error.actual.length > maxItemLength) ? `${error.actual.slice(0, maxItemLength) }...` : error.actual; message += `"${errorItem}" is not a valid URL.`; } // istanbul ignore else @@ -595,9 +620,9 @@ export class TsCSVEntryComponent implements OnInit, OnDestroy { } // If more messages than allowed exist, truncate the list with a message - if (messages.length > this.maximumValidationMessages) { - const count = messages.length - this.maximumValidationMessages; - messages.length = this.maximumValidationMessages; + if (messages.length > DEFAULT_VALIDATION_MESSAGES_MAX) { + const count = messages.length - DEFAULT_VALIDATION_MESSAGES_MAX; + messages.length = DEFAULT_VALIDATION_MESSAGES_MAX; messages.push(`and ${count} more errors...`); } @@ -627,7 +652,7 @@ export class TsCSVEntryComponent implements OnInit, OnDestroy { public resetTable(): void { this.clearAllRows(); this.clearHeaderCells(); - this.columnCount = this.columnCount || this.defaultColumnCount; + this.columnCount = this.columnCount; this.addRows(this.rowCount, this.columnCount); this.addHeaders(this.columnCount, this.columnHeaders); this.allErrors = null; @@ -640,10 +665,10 @@ export class TsCSVEntryComponent implements OnInit, OnDestroy { * NOTE: This external function and `result` object is needed since `getAllErrors` may be recursive * * @param form - The form - * @return The object of errors + * @return An object containing all errors */ - private getFormErrors(form: FormGroup | FormArray): {[key: string]: any} { - const result: {[key: string]: any} = {}; + private getFormErrors(form: FormGroup | FormArray): {required?: TsCSVRequiredError; url?: TsCSVUrlError} { + const result: {required?: TsCSVRequiredError; url?: TsCSVUrlError} = {}; this.getAllErrors(form, result); return result; } @@ -655,7 +680,7 @@ export class TsCSVEntryComponent implements OnInit, OnDestroy { * @param form - The primary form group * @return An object containing all errors */ - private getAllErrors(form: FormGroup | FormArray, result: {[key: string]: any}): void { + private getAllErrors(form: FormGroup | FormArray, result: {required?: TsCSVRequiredError; url?: TsCSVUrlError}): void { const keys = Object.keys(form.controls); for (let i = 0; i < keys.length; i += 1) { @@ -668,14 +693,16 @@ export class TsCSVEntryComponent implements OnInit, OnDestroy { // istanbul ignore else if (errors) { // Get the record ID from the grandparent control + // tslint:disable-next-line no-any const grandparentControls: any = ctrl.parent.parent.controls; - const rowId: number | undefined = grandparentControls.recordId ? - grandparentControls.recordId.value /* istanbul ignore next - Unreachable */ : undefined; + const rowId: number | undefined = grandparentControls.recordId + ? grandparentControls.recordId.value /* istanbul ignore next - Unreachable */ : undefined; const errorKeys = Object.keys(errors); for (let j = 0; j < errorKeys.length; j += 1) { const errorKey: string = errorKeys[j]; - let error: {[key: string]: any} = errors[errorKeys[j]]; + // tslint:disable-next-line no-any + let error: Record = errors[errorKeys[j]]; // Angular built in required validator only returns a boolean if (typeof error === 'boolean') { @@ -687,7 +714,7 @@ export class TsCSVEntryComponent implements OnInit, OnDestroy { // If the rowId exists, add it to the errors object // istanbul ignore else if (rowId !== undefined) { - error['rowId'] = rowId; + error.rowId = rowId; } // Add this error to the result object @@ -709,7 +736,8 @@ export class TsCSVEntryComponent implements OnInit, OnDestroy { * @param content - The event content * @return An object containing all data */ - private splitContent(content: string, hasHeaders: boolean): {[key: string]: any} { + // tslint:disable-next-line no-any + private splitContent(content: string, hasHeaders: boolean): Record { const result: {headers: undefined|string[]; rows: undefined|string[]|string[][]} = { headers: undefined, rows: undefined, @@ -718,9 +746,9 @@ export class TsCSVEntryComponent implements OnInit, OnDestroy { if (hasHeaders) { result.headers = rows[0].split('\t'); - result.rows = rows.slice(1, rows.length).map((r) => r.split('\t')); + result.rows = rows.slice(1, rows.length).map(r => r.split('\t')); } else { - result.rows = rows.slice(0, rows.length).map((r) => r.split('\t')); + result.rows = rows.slice(0, rows.length).map(r => r.split('\t')); } return result; @@ -837,18 +865,18 @@ export class TsCSVEntryComponent implements OnInit, OnDestroy { */ private generateBlob(content: TsCSVFormContents): Blob { const prefix = 'data:text/csv;charset=utf-8,'; - const headers: string = content.headers.join('\t') + '\r\n'; - const rows: string = content.records.map((v) => { - // Encapsulate content with quotes and escape any existing quotes - return v.columns.map((column) => column ? `"${column.replace(/"/g, '""')}"` : '').join('\t'); - }).join('\r\n') + '\r\n'; + const headers = `${content.headers.join('\t') }\r\n`; + // Encapsulate content with quotes and escape any existing quotes + const rows = `${content.records.map(v => v.columns.map(column => (column ? `"${column.replace(/"/g, '""')}"` : '')).join('\t')).join('\r\n') }\r\n`; let joined: string = prefix + headers + rows; // istanbul ignore else if (this.outputFormat === 'csv') { joined = JSON.stringify(joined).replace(/\\t/g, ','); } - return new Blob([joined], {type: 'text/csv'}); + return new Blob([joined], { + type: 'text/csv', + }); } } diff --git a/terminus-ui/date-range/src/date-range.component.spec.ts b/terminus-ui/date-range/src/date-range.component.spec.ts index 0d81b0c2f..2111ea747 100644 --- a/terminus-ui/date-range/src/date-range.component.spec.ts +++ b/terminus-ui/date-range/src/date-range.component.spec.ts @@ -1,17 +1,17 @@ // tslint:disable: no-non-null-assertion -import { Provider, Type } from '@angular/core'; import { - ComponentFixture, -} from '@angular/core/testing'; + Provider, + Type, +} from '@angular/core'; +import { ComponentFixture } from '@angular/core/testing'; import { ReactiveFormsModule } from '@angular/forms'; import { createComponent as createComponentInner, typeInElement, - } from '@terminus/ngx-tools/testing'; +} from '@terminus/ngx-tools/testing'; import * as testComponents from '@terminus/ui/date-range/testing'; -import { - getRangeInputInstances, -} from '@terminus/ui/date-range/testing'; +// eslint-disable-next-line no-duplicate-imports +import { getRangeInputInstances } from '@terminus/ui/date-range/testing'; import { TsDateRangeModule } from './date-range.module'; @@ -100,8 +100,12 @@ describe(`TsDateRangeComponent`, function() { endInputInstance.inputElement.nativeElement.blur(); fixture.detectChanges(); - expect(startInputInstance.formControl.errors).toEqual({required: true}); - expect(endInputInstance.formControl.errors).toEqual({required: true}); + expect(startInputInstance.formControl.errors).toEqual({ + required: true, + }); + expect(endInputInstance.formControl.errors).toEqual({ + required: true, + }); jest.runAllTimers(); expect.assertions(4); }); @@ -167,12 +171,18 @@ describe(`TsDateRangeComponent`, function() { typeInElement('3-4-2019', startInputInstance.inputElement.nativeElement); fixture.detectChanges(); expect(fixture.componentInstance.startSelected).toHaveBeenCalledWith(new Date('3-4-2019')); - expect(fixture.componentInstance.dateRangeChange).toHaveBeenCalledWith({start: new Date('3-4-2019'), end: null}); + expect(fixture.componentInstance.dateRangeChange).toHaveBeenCalledWith({ + start: new Date('3-4-2019'), + end: null, + }); typeInElement('3-8-2019', endInputInstance.inputElement.nativeElement); fixture.detectChanges(); expect(fixture.componentInstance.endSelected).toHaveBeenCalledWith(new Date('3-8-2019')); - expect(fixture.componentInstance.dateRangeChange).toHaveBeenCalledWith({start: new Date('3-4-2019'), end: new Date('3-8-2019')}); + expect(fixture.componentInstance.dateRangeChange).toHaveBeenCalledWith({ + start: new Date('3-4-2019'), + end: new Date('3-8-2019'), + }); typeInElement('', startInputInstance.inputElement.nativeElement); fixture.detectChanges(); @@ -181,7 +191,10 @@ describe(`TsDateRangeComponent`, function() { const changeMock = fixture.componentInstance.dateRangeChange.mock; // FIXME: Once https://github.com/GetTerminus/terminus-ui/issues/1361 is complete we should adjust this // test to verify that the changeMock was called exactly 3 times. - expect(changeMock.calls[changeMock.calls.length - 1][0]).toEqual({start: null, end: new Date('3-8-2019')}); + expect(changeMock.calls[changeMock.calls.length - 1][0]).toEqual({ + start: null, + end: new Date('3-8-2019'), + }); expect.assertions(5); }); diff --git a/terminus-ui/date-range/src/date-range.component.ts b/terminus-ui/date-range/src/date-range.component.ts index cc1928438..14dd37139 100644 --- a/terminus-ui/date-range/src/date-range.component.ts +++ b/terminus-ui/date-range/src/date-range.component.ts @@ -1,4 +1,5 @@ import { + ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, @@ -66,6 +67,7 @@ export interface TsDateRange { host: { class: 'ts-date-range', }, + changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, exportAs: 'tsDateRange', }) @@ -95,6 +97,7 @@ export class TsDateRangeComponent implements OnInit, OnDestroy { * * NOTE: `any` is used since we cannot seem to use union types in a BehaviorSubject and the value could be a Date or undefined */ + // tslint:disable-next-line no-any public endMinDate$: BehaviorSubject = new BehaviorSubject(undefined); /** @@ -130,6 +133,7 @@ export class TsDateRangeComponent implements OnInit, OnDestroy { * * NOTE: `any` is used since we cannot seem to use union types in a BehaviorSubject and the value could be a Date or undefined */ + // tslint:disable-next-line no-any public startMaxDate$: BehaviorSubject = new BehaviorSubject(undefined); /** @@ -195,24 +199,26 @@ export class TsDateRangeComponent implements OnInit, OnDestroy { * Event emitted anytime the range is changed */ @Output() - public dateRangeChange: EventEmitter = new EventEmitter(); + public readonly dateRangeChange: EventEmitter = new EventEmitter(); /** * Output the end date when selected */ @Output() - public endSelected: EventEmitter = new EventEmitter(); + public readonly endSelected: EventEmitter = new EventEmitter(); /** * Output the start date when selected */ @Output() - public startSelected: EventEmitter = new EventEmitter(); + public readonly startSelected: EventEmitter = new EventEmitter(); + constructor( private changeDetectorRef: ChangeDetectorRef, ) { } + /** * Seed initial date range values */ @@ -265,7 +271,7 @@ export class TsDateRangeComponent implements OnInit, OnDestroy { this.internalEndControl.setValue(endCtrl.value); // START DATE - startCtrl.valueChanges.pipe(untilComponentDestroyed(this)).subscribe((value) => { + startCtrl.valueChanges.pipe(untilComponentDestroyed(this)).subscribe(value => { this.internalStartControl.setValue(value); this.endMinDate$.next(value); }); @@ -274,7 +280,7 @@ export class TsDateRangeComponent implements OnInit, OnDestroy { }); // END DATE - endCtrl.valueChanges.pipe(untilComponentDestroyed(this)).subscribe((value) => { + endCtrl.valueChanges.pipe(untilComponentDestroyed(this)).subscribe(value => { this.internalEndControl.setValue(value); this.startMaxDate$.next(value); }); diff --git a/terminus-ui/date-range/testing/src/test-helpers.ts b/terminus-ui/date-range/testing/src/test-helpers.ts index c89475730..b929dded4 100644 --- a/terminus-ui/date-range/testing/src/test-helpers.ts +++ b/terminus-ui/date-range/testing/src/test-helpers.ts @@ -31,7 +31,7 @@ export function getAllDateRangeDebugElements(fixture: ComponentFixture): De * @return The array of TsDateRangeComponent instances */ export function getAllDateRangeInstances(fixture: ComponentFixture): TsDateRangeComponent[] { - return getAllDateRangeDebugElements(fixture).map((i) => i.componentInstance); + return getAllDateRangeDebugElements(fixture).map(i => i.componentInstance); } /** @@ -57,7 +57,7 @@ export function getDateRangeInputDebugElements(fixture: ComponentFixture, i * @return The array of TsInputComponent instances */ export function getRangeInputInstances(fixture: ComponentFixture, index = 0): TsInputComponent[] { - return getDateRangeInputDebugElements(fixture, index).map((v) => v.componentInstance); + return getDateRangeInputDebugElements(fixture, index).map(v => v.componentInstance); } /** @@ -68,7 +68,7 @@ export function getRangeInputInstances(fixture: ComponentFixture, index = 0 * @return The array of TsInputComponent instances */ export function getRangeInputElements(fixture: ComponentFixture, index = 0): HTMLInputElement[] { - return getDateRangeInputDebugElements(fixture, index).map((v) => v.componentInstance.inputElement.nativeElement); + return getDateRangeInputDebugElements(fixture, index).map(v => v.componentInstance.inputElement.nativeElement); } /** diff --git a/terminus-ui/expansion-panel/src/accordion/accordion-base.ts b/terminus-ui/expansion-panel/src/accordion/accordion-base.ts index 26e4d3fa2..d7d0753ad 100644 --- a/terminus-ui/expansion-panel/src/accordion/accordion-base.ts +++ b/terminus-ui/expansion-panel/src/accordion/accordion-base.ts @@ -1,6 +1,8 @@ import { CdkAccordion } from '@angular/cdk/accordion'; import { InjectionToken } from '@angular/core'; +import { TsExpansionPanelTriggerComponent } from './../trigger/expansion-panel-trigger.component'; + /** * Base interface for a {@link TsAccordionComponent} @@ -14,7 +16,7 @@ export interface TsAccordionBase extends CdkAccordion { /** * Handle focus events on the panel triggers */ - handleTriggerFocus: (trigger: any) => void; + handleTriggerFocus: (trigger: TsExpansionPanelTriggerComponent) => void; /** * Determine if the toggle icon should be hidden diff --git a/terminus-ui/expansion-panel/src/accordion/accordion.component.spec.ts b/terminus-ui/expansion-panel/src/accordion/accordion.component.spec.ts index fde60d4f6..9923395cd 100644 --- a/terminus-ui/expansion-panel/src/accordion/accordion.component.spec.ts +++ b/terminus-ui/expansion-panel/src/accordion/accordion.component.spec.ts @@ -7,6 +7,7 @@ import { createKeyboardEvent, } from '@terminus/ngx-tools/testing'; import * as testComponents from '@terminus/ui/expansion-panel/testing'; +// eslint-disable-next-line no-duplicate-imports import { getAccordionElement, getAccordionInstance, @@ -153,14 +154,22 @@ describe(`TsAccordionComponent`, function() { const spaceEvent = document.createEvent('KeyboardEvent'); spaceEvent.initEvent('keydown', true, false); Object.defineProperties(spaceEvent, { - code: { get: () => KEYS.SPACE.code }, - key: { get: () => KEYS.SPACE.code }, + code: { + get: () => KEYS.SPACE.code, + }, + key: { + get: () => KEYS.SPACE.code, + }, }); const enterEvent = document.createEvent('KeyboardEvent'); enterEvent.initEvent('keydown', true, false); Object.defineProperties(enterEvent, { - code: { get: () => KEYS.ENTER.code }, - key: { get: () => KEYS.ENTER.code }, + code: { + get: () => KEYS.ENTER.code, + }, + key: { + get: () => KEYS.ENTER.code, + }, }); trigger1.dispatchEvent(spaceEvent); diff --git a/terminus-ui/expansion-panel/src/accordion/accordion.component.ts b/terminus-ui/expansion-panel/src/accordion/accordion.component.ts index fce69e23c..e5befe9db 100644 --- a/terminus-ui/expansion-panel/src/accordion/accordion.component.ts +++ b/terminus-ui/expansion-panel/src/accordion/accordion.component.ts @@ -14,7 +14,7 @@ import { } from '@angular/core'; import { KEYS } from '@terminus/ngx-tools/keycodes'; import { TsExpansionPanelComponent } from '../expansion-panel.component'; -import { TsExpansionPanelTriggerComponent } from '../trigger/expansion-panel-trigger.component'; +import { TsExpansionPanelTriggerComponent } from './../trigger/expansion-panel-trigger.component'; import { TS_ACCORDION, TsAccordionBase, @@ -49,7 +49,7 @@ import { selector: 'ts-accordion', template: ``, // NOTE: @Inputs are defined here rather than using decorators since we are extending the @Inputs of the base class - // tslint:disable: use-input-property-decorator + // tslint:disable-next-line:no-inputs-metadata-property inputs: ['multi'], providers: [ { @@ -73,7 +73,9 @@ export class TsAccordionComponent extends CdkAccordion implements TsAccordionBas /** * Collect a list of all triggers */ - @ContentChildren(TsExpansionPanelTriggerComponent, {descendants: true}) + @ContentChildren(TsExpansionPanelTriggerComponent, { + descendants: true, + }) public triggers!: QueryList; /** @@ -92,7 +94,7 @@ export class TsAccordionComponent extends CdkAccordion implements TsAccordionBas * The event emitted as the accordion is destroyed */ @Output() - public destroyed: EventEmitter = new EventEmitter(); + public readonly destroyed: EventEmitter = new EventEmitter(); /** diff --git a/terminus-ui/expansion-panel/src/expansion-animations.ts b/terminus-ui/expansion-panel/src/expansion-animations.ts index 04290cb03..c4b0286f2 100644 --- a/terminus-ui/expansion-panel/src/expansion-animations.ts +++ b/terminus-ui/expansion-panel/src/expansion-animations.ts @@ -46,8 +46,12 @@ export const tsExpansionPanelAnimations: { * Animation that rotates the indicator arrow */ indicatorRotate: trigger('indicatorRotate', [ - state('collapsed, void', style({transform: 'rotate(0deg)'})), - state('expanded', style({transform: 'rotate(180deg)'})), + state('collapsed, void', style({ + transform: 'rotate(0deg)', + })), + state('expanded', style({ + transform: 'rotate(180deg)', + })), transition('expanded <=> collapsed, void => collapsed', animate(TS_EXPANSION_PANEL_ANIMATION_TIMING)), ]), @@ -59,15 +63,21 @@ export const tsExpansionPanelAnimations: { state('collapsed, void', style({ height: '{{collapsedHeight}}', }), { - params: {collapsedHeight: '48px'}, + params: { + collapsedHeight: '48px', + }, }), state('expanded', style({ height: '{{expandedHeight}}', }), { - params: {expandedHeight: '64px'}, + params: { + expandedHeight: '64px', + }, }), transition('expanded <=> collapsed, void => collapsed', group([ - query('@indicatorRotate', animateChild(), {optional: true}), + query('@indicatorRotate', animateChild(), { + optional: true, + }), animate(TS_EXPANSION_PANEL_ANIMATION_TIMING), ])), ]), @@ -76,9 +86,15 @@ export const tsExpansionPanelAnimations: { * Animation that expands and collapses the panel content */ bodyExpansion: trigger('bodyExpansion', [ - state('collapsed, void', style({height: '0px', visibility: 'hidden'})), - state('expanded', style({height: '*', visibility: 'visible'})), + state('collapsed, void', style({ + height: '0px', + visibility: 'hidden', + })), + state('expanded', style({ + height: '*', + visibility: 'visible', + })), transition('expanded <=> collapsed, void => collapsed', - animate(TS_EXPANSION_PANEL_ANIMATION_TIMING)), + animate(TS_EXPANSION_PANEL_ANIMATION_TIMING)), ]), }; diff --git a/terminus-ui/expansion-panel/src/expansion-panel-action-row.ts b/terminus-ui/expansion-panel/src/expansion-panel-action-row.ts index bb7afa6ec..7a22e276d 100644 --- a/terminus-ui/expansion-panel/src/expansion-panel-action-row.ts +++ b/terminus-ui/expansion-panel/src/expansion-panel-action-row.ts @@ -1,4 +1,8 @@ -import { Component } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + ViewEncapsulation, +} from '@angular/core'; /** @@ -21,9 +25,12 @@ import { Component } from '@angular/core'; */ @Component({ selector: 'ts-expansion-panel-action-row', - template: ``, + template: ``, host: { class: 'ts-expansion-panel__action-row', }, + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None, + exportAs: 'tsExpansionPanelActionRow', }) export class TsExpansionPanelActionRowComponent {} diff --git a/terminus-ui/expansion-panel/src/expansion-panel-content.directive.ts b/terminus-ui/expansion-panel/src/expansion-panel-content.directive.ts index 5ff171b8a..fffe2ab6f 100644 --- a/terminus-ui/expansion-panel/src/expansion-panel-content.directive.ts +++ b/terminus-ui/expansion-panel/src/expansion-panel-content.directive.ts @@ -24,5 +24,6 @@ import { selector: 'ng-template[tsExpansionPanelContent]', }) export class TsExpansionPanelContentDirective { + // tslint:disable-next-line no-any constructor(public template: TemplateRef) {} } diff --git a/terminus-ui/expansion-panel/src/expansion-panel.component.spec.ts b/terminus-ui/expansion-panel/src/expansion-panel.component.spec.ts index 68aacdb24..44f70a5cf 100644 --- a/terminus-ui/expansion-panel/src/expansion-panel.component.spec.ts +++ b/terminus-ui/expansion-panel/src/expansion-panel.component.spec.ts @@ -3,6 +3,7 @@ import { ComponentFixture } from '@angular/core/testing'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { createComponent as createComponentInner } from '@terminus/ngx-tools/testing'; import * as testComponents from '@terminus/ui/expansion-panel/testing'; +// eslint-disable-next-line no-duplicate-imports import { getPanelActionRow, getPanelBodyContentElement, @@ -125,7 +126,9 @@ describe(`TsExpansionPanelComponent`, function() { const trigger = getTriggerInstance(fixture); const panel = getPanelInstance(fixture); Object.defineProperties(panel, { - contentContainsFocus: { get: () => true }, + contentContainsFocus: { + get: () => true, + }, }); trigger['focusMonitor'].focusVia = jest.fn(); diff --git a/terminus-ui/expansion-panel/src/expansion-panel.component.ts b/terminus-ui/expansion-panel/src/expansion-panel.component.ts index 03b3c24da..784860e4b 100644 --- a/terminus-ui/expansion-panel/src/expansion-panel.component.ts +++ b/terminus-ui/expansion-panel/src/expansion-panel.component.ts @@ -36,7 +36,9 @@ import { take, } from 'rxjs/operators'; -import { TS_ACCORDION, TsAccordionBase } from './accordion/accordion-base'; +import { + TS_ACCORDION, TsAccordionBase, +} from './accordion/accordion-base'; import { tsExpansionPanelAnimations } from './expansion-animations'; import { TsExpansionPanelContentDirective } from './expansion-panel-content.directive'; @@ -108,11 +110,16 @@ let nextUniqueId = 0; templateUrl: './expansion-panel.component.html', styleUrls: ['./expansion-panel.component.scss'], // NOTE: @Outputs are defined here rather than using decorators since we are extending the @Outputs of the base class - // tslint:disable: use-output-property-decorator - outputs: ['opened', 'closed', 'expandedChange', 'destroyed'], + // tslint:disable-next-line:no-outputs-metadata-property + outputs: [ + 'opened', + 'closed', + 'expandedChange', + 'destroyed', + ], animations: [tsExpansionPanelAnimations.bodyExpansion], host: { - class: 'ts-expansion-panel', + 'class': 'ts-expansion-panel', '[class.ts-expansion-panel--expanded]': 'expanded', '[class.ts-expansion-panel--animation-noopable]': 'animationMode === "NoopAnimations"', }, @@ -151,7 +158,7 @@ export class TsExpansionPanelComponent extends CdkAccordionItem implements After /** * Stream that emits for changes in `@Input` properties */ - readonly inputChanges = new Subject(); + public readonly inputChanges = new Subject(); /** * Optionally defined accordion the expansion panel belongs to @@ -237,19 +244,19 @@ export class TsExpansionPanelComponent extends CdkAccordionItem implements After * The event emitted after the panel body's expansion animation finishes */ @Output() - readonly afterExpand: EventEmitter = new EventEmitter(); + public readonly afterExpand: EventEmitter = new EventEmitter(); /** * The event emitted after the panel body's collapse animation finishes */ @Output() - readonly afterCollapse: EventEmitter = new EventEmitter(); + public readonly afterCollapse: EventEmitter = new EventEmitter(); constructor( @Optional() @SkipSelf() @Inject(TS_ACCORDION) accordion: TsAccordionBase, - _changeDetectorRef: ChangeDetectorRef, - _uniqueSelectionDispatcher: UniqueSelectionDispatcher, + _changeDetectorRef: ChangeDetectorRef, + _uniqueSelectionDispatcher: UniqueSelectionDispatcher, private _viewContainerRef: ViewContainerRef, private documentService: TsDocumentService, @Optional() @Inject(ANIMATION_MODULE_TYPE) public animationMode?: string, @@ -264,10 +271,8 @@ export class TsExpansionPanelComponent extends CdkAccordionItem implements After // See https://github.com/angular/angular/issues/24084 this.bodyAnimationDone.pipe( untilComponentDestroyed(this), - distinctUntilChanged((x, y) => { - return x.fromState === y.fromState && x.toState === y.toState; - }), - ).subscribe((event) => { + distinctUntilChanged((x, y) => x.fromState === y.fromState && x.toState === y.toState), + ).subscribe(event => { // istanbul ignore else if (event.fromState !== 'void') { if (event.toState === 'expanded') { diff --git a/terminus-ui/expansion-panel/src/trigger/expansion-panel-trigger-description.component.ts b/terminus-ui/expansion-panel/src/trigger/expansion-panel-trigger-description.component.ts index 5bb5841a5..37a0620a8 100644 --- a/terminus-ui/expansion-panel/src/trigger/expansion-panel-trigger-description.component.ts +++ b/terminus-ui/expansion-panel/src/trigger/expansion-panel-trigger-description.component.ts @@ -1,4 +1,8 @@ -import { Component } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + ViewEncapsulation, +} from '@angular/core'; @Component({ @@ -7,5 +11,8 @@ import { Component } from '@angular/core'; host: { class: 'ts-expansion-panel__trigger-description', }, + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None, + exportAs: 'tsExpansionPanelTriggerDescription', }) export class TsExpansionPanelTriggerDescriptionComponent {} diff --git a/terminus-ui/expansion-panel/src/trigger/expansion-panel-trigger-title.component.ts b/terminus-ui/expansion-panel/src/trigger/expansion-panel-trigger-title.component.ts index f3e3c1a39..e59b2517a 100644 --- a/terminus-ui/expansion-panel/src/trigger/expansion-panel-trigger-title.component.ts +++ b/terminus-ui/expansion-panel/src/trigger/expansion-panel-trigger-title.component.ts @@ -1,4 +1,8 @@ -import { Component } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + ViewEncapsulation, +} from '@angular/core'; @@ -8,5 +12,8 @@ import { Component } from '@angular/core'; host: { class: 'ts-expansion-panel__trigger-title', }, + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None, + exportAs: 'tsExpansionPanelTriggerTitle', }) export class TsExpansionPanelTriggerTitleComponent {} diff --git a/terminus-ui/expansion-panel/src/trigger/expansion-panel-trigger.component.ts b/terminus-ui/expansion-panel/src/trigger/expansion-panel-trigger.component.ts index 16d84e425..167ef4a60 100644 --- a/terminus-ui/expansion-panel/src/trigger/expansion-panel-trigger.component.ts +++ b/terminus-ui/expansion-panel/src/trigger/expansion-panel-trigger.component.ts @@ -24,12 +24,12 @@ import { } from 'rxjs'; import { filter } from 'rxjs/operators'; -import { tsExpansionPanelAnimations } from '../expansion-animations'; +import { tsExpansionPanelAnimations } from './../expansion-animations'; import { TS_EXPANSION_PANEL_DEFAULT_OPTIONS, TsExpansionPanelComponent, TsExpansionPanelDefaultOptions, -} from '../expansion-panel.component'; +} from './../expansion-panel.component'; /** @@ -58,8 +58,8 @@ import { styleUrls: ['./expansion-panel-trigger.component.scss'], templateUrl: './expansion-panel-trigger.component.html', host: { - class: 'ts-expansion-panel__trigger', - role: 'button', + 'class': 'ts-expansion-panel__trigger', + 'role': 'button', '[attr.id]': 'panel.triggerId', '[attr.tabindex]': 'disabled ? -1 : 0', '[attr.aria-controls]': 'panel.id', @@ -84,14 +84,14 @@ export class TsExpansionPanelTriggerComponent implements OnDestroy, FocusableOpt /** * Determine the current expanded state string of the panel */ - get currentPanelExpandedState(): string { + public get currentPanelExpandedState(): string { return this.panel.currentExpandedState; } /** * Determine if the panel is currently expanded */ - get isExpanded(): boolean { + public get isExpanded(): boolean { return this.panel.expanded; } @@ -100,12 +100,12 @@ export class TsExpansionPanelTriggerComponent implements OnDestroy, FocusableOpt * * Implemented as a part of `FocusableOption`. */ - get disabled(): boolean { + public get disabled(): boolean { return this.panel.disabled; } /** Gets whether the expand indicator should be shown. */ - get shouldShowToggle(): boolean { + public get shouldShowToggle(): boolean { return !this.panel.hideToggle && !this.panel.disabled; } @@ -113,13 +113,13 @@ export class TsExpansionPanelTriggerComponent implements OnDestroy, FocusableOpt * Height of the trigger while the panel is collapsed */ @Input() - collapsedHeight: string | undefined; + public collapsedHeight: string | undefined; /** * Height of the trigger while the panel is expanded */ @Input() - expandedHeight: string | undefined; + public expandedHeight: string | undefined; constructor( @@ -130,18 +130,20 @@ export class TsExpansionPanelTriggerComponent implements OnDestroy, FocusableOpt @Inject(TS_EXPANSION_PANEL_DEFAULT_OPTIONS) @Optional() defaultOptions?: TsExpansionPanelDefaultOptions, ) { const accordionHideToggleChange = - panel.accordion ? - panel.accordion._stateChanges.pipe(filter((changes) => !!changes['hideToggle'])) : - EMPTY; + panel.accordion + // NOTE: Underscore naming controlled by Material + // eslint-disable-next-line no-underscore-dangle + ? panel.accordion._stateChanges.pipe(filter(changes => !!changes.hideToggle)) + : EMPTY; // Since the toggle state depends on an @Input on the panel, we need to subscribe and trigger change detection manually. merge( panel.opened, panel.closed, accordionHideToggleChange, - panel.inputChanges.pipe(filter((changes) => !!(changes['hideToggle'] || changes['disabled']))), + panel.inputChanges.pipe(filter(changes => !!(changes.hideToggle || changes.disabled))), ).pipe( untilComponentDestroyed(this), ) - .subscribe(() => this.changeDetectorRef.markForCheck()); + .subscribe(() => this.changeDetectorRef.markForCheck()); // Avoid focus being lost if the panel contained the focused element and was closed. panel.closed.pipe( @@ -150,7 +152,7 @@ export class TsExpansionPanelTriggerComponent implements OnDestroy, FocusableOpt ).subscribe(() => focusMonitor.focusVia(elementRef, 'program')); // Subscribe to trigger focus events - focusMonitor.monitor(elementRef).subscribe((origin) => { + focusMonitor.monitor(elementRef).subscribe(origin => { if (origin && panel.accordion) { panel.accordion.handleTriggerFocus(this); } @@ -204,11 +206,8 @@ export class TsExpansionPanelTriggerComponent implements OnDestroy, FocusableOpt event.preventDefault(); this.toggle(); } - } else { - // istanbul ignore else - if (this.panel.accordion) { - this.panel.accordion.handleTriggerKeydown(event); - } + } else if (this.panel.accordion) { + this.panel.accordion.handleTriggerKeydown(event); } } diff --git a/terminus-ui/expansion-panel/testing/src/test-components.ts b/terminus-ui/expansion-panel/testing/src/test-components.ts index 7297bc8f8..859984446 100644 --- a/terminus-ui/expansion-panel/testing/src/test-components.ts +++ b/terminus-ui/expansion-panel/testing/src/test-components.ts @@ -1,9 +1,13 @@ // tslint:disable: component-class-suffix import { CommonModule } from '@angular/common'; -import { Component, NgModule } from '@angular/core'; +import { + Component, NgModule, +} from '@angular/core'; import { noop } from '@terminus/ngx-tools'; -import { TS_EXPANSION_PANEL_DEFAULT_OPTIONS, TsExpansionPanelModule } from '@terminus/ui/expansion-panel'; +import { + TS_EXPANSION_PANEL_DEFAULT_OPTIONS, TsExpansionPanelModule, +} from '@terminus/ui/expansion-panel'; @Component({ @@ -201,7 +205,7 @@ export class Events { afterExpand = noop; afterCollapse = noop; destroyed = noop; - expandedChange = (v) => {}; + expandedChange = v => {}; } @Component({ diff --git a/terminus-ui/expansion-panel/testing/src/test-helpers.ts b/terminus-ui/expansion-panel/testing/src/test-helpers.ts index 523e634f9..da6519689 100644 --- a/terminus-ui/expansion-panel/testing/src/test-helpers.ts +++ b/terminus-ui/expansion-panel/testing/src/test-helpers.ts @@ -29,7 +29,7 @@ export function getAllPanelInstances(fixture: ComponentFixture): TsExpansio if (debugElements.length < 1) { throw new Error(`getAllPanelInstances did not find any instances`); } - return debugElements.map((debugElement) => debugElement.componentInstance); + return debugElements.map(debugElement => debugElement.componentInstance); } /** @@ -117,7 +117,7 @@ export function getAllAccordionInstances(fixture: ComponentFixture): TsAcco if (debugElements.length < 1) { throw new Error(`getAllAccordionInstances did not find any instances`); } - return debugElements.map((debugElement) => debugElement.componentInstance); + return debugElements.map(debugElement => debugElement.componentInstance); } /** diff --git a/terminus-ui/file-upload/src/drop-protection.service.ts b/terminus-ui/file-upload/src/drop-protection.service.ts index 80c9a85b1..451683450 100644 --- a/terminus-ui/file-upload/src/drop-protection.service.ts +++ b/terminus-ui/file-upload/src/drop-protection.service.ts @@ -15,7 +15,7 @@ export class TsDropProtectionService { /** * Add drop protection */ - add(): void { + public add(): void { if (!this.hasProtection) { this.windowService.nativeWindow.addEventListener('dragover', this.prevent, false); this.windowService.nativeWindow.addEventListener('drop', this.prevent, false); @@ -27,7 +27,7 @@ export class TsDropProtectionService { /** * Remove drop protection */ - remove(): void { + public remove(): void { if (this.hasProtection) { this.windowService.nativeWindow.removeEventListener('dragover', this.prevent, false); this.windowService.nativeWindow.removeEventListener('drop', this.prevent, false); @@ -43,7 +43,7 @@ export class TsDropProtectionService { * * @param event - The event */ - prevent(e: Event): void { + public prevent(e: Event): void { e.preventDefault(); } diff --git a/terminus-ui/file-upload/src/file-upload.component.html b/terminus-ui/file-upload/src/file-upload.component.html index a6fc512b7..150567b95 100644 --- a/terminus-ui/file-upload/src/file-upload.component.html +++ b/terminus-ui/file-upload/src/file-upload.component.html @@ -116,7 +116,7 @@
{{ 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 4de6983ae..d0815c71f 100644 --- a/terminus-ui/file-upload/src/file-upload.component.spec.ts +++ b/terminus-ui/file-upload/src/file-upload.component.spec.ts @@ -22,18 +22,21 @@ 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 } from './mime-types'; +import { + TS_ACCEPTED_MIME_TYPES, TsFileAcceptedMimeTypes, +} from './mime-types'; import { TsSelectedFile } from './selected-file'; -// tslint:disable: max-line-length +// eslint-disable-next-line max-len const fileContentsMock = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABIAQMAAABvIyEEAAAAA1BMVEXXbFn0Q9OUAAAADklEQVR4AWMYRmAUjAIAAtAAAaW+yXMAAAAASUVORK5CYII='; -// tslint:enable: max-line-length // IMAGE MOCK const FILE_BLOB = new Blob( [fileContentsMock], - { type: 'image/png' }, + { + type: 'image/png', + }, ); FILE_BLOB['lastModifiedDate'] = new Date(); FILE_BLOB['name'] = 'foo'; @@ -63,25 +66,25 @@ const FILE_MOCK = FILE_BLOB as File; `, }) class TestHostComponent { - mimeTypes: TsFileAcceptedMimeTypes | TsFileAcceptedMimeTypes[] | undefined = ['image/png', 'image/jpg']; - maxKb: number | undefined; - multiple = false; - progress: number | undefined; - fileToSeed: File | undefined; - constraints: TsFileImageDimensionConstraints | undefined; - ratioConstraints: Array | undefined; - theme: TsStyleThemeTypes | undefined; - hideButton = false; - formControl = new FormControl('test'); + public mimeTypes: TsFileAcceptedMimeTypes | TsFileAcceptedMimeTypes[] | undefined = ['image/png', 'image/jpg']; + public maxKb: number | undefined; + public multiple = false; + public progress: number | undefined; + public fileToSeed: File | undefined; + public constraints: TsFileImageDimensionConstraints | undefined; + public ratioConstraints: Array | undefined; + public theme: TsStyleThemeTypes | undefined; + public hideButton = false; + public formControl = new FormControl('test'); @ViewChild(TsFileUploadComponent) - component!: TsFileUploadComponent; + public component!: TsFileUploadComponent; - userDragBegin = jest.fn(); - userDragEnd = jest.fn(); - handleFile = jest.fn(); - handleMultipleFiles = jest.fn(); - cleared = jest.fn(); + public userDragBegin = jest.fn(); + public userDragEnd = jest.fn(); + public handleFile = jest.fn(); + public handleMultipleFiles = jest.fn(); + public cleared = jest.fn(); } @@ -118,25 +121,34 @@ describe(`TsFileUploadComponent`, function() { // Mock FileReader class DummyFileReader { - addEventListener = jest.fn(); - readAsDataURL = jest.fn().mockImplementation(function(this: FileReader) { this.onload({} as Event); }); - // tslint:disable: max-line-length - result = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABIAQMAAABvIyEEAAAAA1BMVEXXbFn0Q9OUAAAADklEQVR4AWMYRmAUjAIAAtAAAaW+yXMAAAAASUVORK5CYII='; - // tslint:enable: max-line-length + public addEventListener = jest.fn(); + public readAsDataURL = jest.fn().mockImplementation(function(this: FileReader) { + this.onload({} as Event); + }); + // eslint-disable-next-line max-len + public result = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABIAQMAAABvIyEEAAAAA1BMVEXXbFn0Q9OUAAAADklEQVR4AWMYRmAUjAIAAtAAAaW+yXMAAAAASUVORK5CYII='; } // Not sure why any is needed (window as any).FileReader = jest.fn(() => new DummyFileReader); class DummyImage { - _onload = () => {}; - set onload(fn) { this._onload = fn; } - get onload() { return this._onload; } - set src(source) { + public _onload = () => {}; + public set onload(fn) { + this._onload = fn; + } + public get onload() { + return this._onload; + } + public set src(source) { this.onload(); } - get naturalWidth() { return 100; } - get naturalHeight() { return 100; } + public get naturalWidth() { + return 100; + } + public get naturalHeight() { + return 100; + } } (window as any).Image = jest.fn(() => new DummyImage()); @@ -542,180 +554,188 @@ describe(`TsFileUploadComponent`, function() { }); - describe(`collectFilesFromEvent`, () => { + describe(`collectFilesFromEvent`, () => { - 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); }).toThrowError(); - fixture.detectChanges(); - - expect(component['setUpNewFile']).not.toHaveBeenCalled(); - expect(hostComponent.handleFile).not.toHaveBeenCalled(); + 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); + }).toThrowError(); + fixture.detectChanges(); + expect(component['setUpNewFile']).not.toHaveBeenCalled(); + expect(hostComponent.handleFile).not.toHaveBeenCalled(); + }); - test(`should throw an error if no files exist on the event target`, () => { - const event = createFakeEvent('Event'); - const input = document.createElement('input'); - Object.defineProperty(event, 'target', { - value: input, - }); - component['setUpNewFile'] = jest.fn(); - expect(() => { component['collectFilesFromEvent'](event); }).toThrowError(); - fixture.detectChanges(); - expect(component['setUpNewFile']).not.toHaveBeenCalled(); - expect(hostComponent.handleFile).not.toHaveBeenCalled(); + test(`should throw an error if no files exist on the event target`, () => { + const event = createFakeEvent('Event'); + const input = document.createElement('input'); + Object.defineProperty(event, 'target', { + value: input, }); + component['setUpNewFile'] = jest.fn(); + expect(() => { + component['collectFilesFromEvent'](event); + }).toThrowError(); + fixture.detectChanges(); + 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); - fixture.detectChanges(); - 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 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); + fixture.detectChanges(); + 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); - fixture.detectChanges(); - expect(component['setUpNewFile']).toHaveBeenCalledWith(expect.any(TsSelectedFile)); - expect(hostComponent.handleFile).toHaveBeenCalledWith(expect.any(TsSelectedFile)); + 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); + fixture.detectChanges(); + 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); - fixture.detectChanges(); - expect(hostComponent.handleMultipleFiles).toHaveBeenCalled(); - expect(hostComponent.handleFile).not.toHaveBeenCalled(); - expect(component['setUpNewFile']).not.toHaveBeenCalled(); + 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); + fixture.detectChanges(); + expect(hostComponent.handleMultipleFiles).toHaveBeenCalled(); + expect(hostComponent.handleFile).not.toHaveBeenCalled(); + expect(component['setUpNewFile']).not.toHaveBeenCalled(); }); + }); - describe(`ngOnDestroy`, () => { - test(`should remove the event listener`, () => { - component['onVirtualInputElementChange'] = jest.fn(); - component['dropProtectionService'].remove = jest.fn(); - component.ngOnDestroy(); - const event = createFakeEvent('change'); - component['virtualFileInput'].dispatchEvent(event); + describe(`ngOnDestroy`, () => { - expect(component['onVirtualInputElementChange']).not.toHaveBeenCalled(); - expect(component['dropProtectionService'].remove).toHaveBeenCalled(); - }); + test(`should remove the event listener`, () => { + component['onVirtualInputElementChange'] = jest.fn(); + component['dropProtectionService'].remove = jest.fn(); + component.ngOnDestroy(); + const event = createFakeEvent('change'); + component['virtualFileInput'].dispatchEvent(event); + expect(component['onVirtualInputElementChange']).not.toHaveBeenCalled(); + expect(component['dropProtectionService'].remove).toHaveBeenCalled(); }); + }); - describe(`virtualFileInput.change`, () => { - test(`should trigger the file handler`, () => { - component['collectFilesFromEvent'] = jest.fn(); - // Wire up bindings - component.ngAfterContentInit(); - const event = createFakeEvent('change'); - component['virtualFileInput'].dispatchEvent(event); + describe(`virtualFileInput.change`, () => { - expect(component['collectFilesFromEvent']).toHaveBeenCalled(); - expect(component['virtualFileInput'].value).toEqual(''); - }); + test(`should trigger the file handler`, () => { + component['collectFilesFromEvent'] = jest.fn(); + // Wire up bindings + component.ngAfterContentInit(); + const event = createFakeEvent('change'); + component['virtualFileInput'].dispatchEvent(event); + expect(component['collectFilesFromEvent']).toHaveBeenCalled(); + expect(component['virtualFileInput'].value).toEqual(''); }); + }); - describe(`preventAndStopEventPropagation`, () => { - test(`should both prevent and stop event propogation`, () => { - const event = createFakeEvent('fake'); - Object.defineProperties(event, { - preventDefault: { value: jest.fn() }, - stopPropagation: { value: jest.fn() }, - }); - component['preventAndStopEventPropagation'](event); + describe(`preventAndStopEventPropagation`, () => { - expect(event.preventDefault).toHaveBeenCalled(); - expect(event.stopPropagation).toHaveBeenCalled(); + test(`should both prevent and stop event propogation`, () => { + const event = createFakeEvent('fake'); + Object.defineProperties(event, { + preventDefault: { + value: jest.fn(), + }, + stopPropagation: { + value: jest.fn(), + }, }); + component['preventAndStopEventPropagation'](event); + expect(event.preventDefault).toHaveBeenCalled(); + expect(event.stopPropagation).toHaveBeenCalled(); }); + }); - describe(`ngOnInit`, () => { - test(`should enable dropProtectionService`, () => { - component['dropProtectionService'].add = jest.fn(); - component.ngOnInit(); + describe(`ngOnInit`, () => { - expect(component['dropProtectionService'].add).toHaveBeenCalled(); - }); + test(`should enable dropProtectionService`, () => { + component['dropProtectionService'].add = jest.fn(); + component.ngOnInit(); + expect(component['dropProtectionService'].add).toHaveBeenCalled(); }); + }); - describe(`theme`, () => { - test(`should set the theme`, () => { - hostComponent.theme = 'warn'; - fixture.detectChanges(); + describe(`theme`, () => { - expect(component.theme).toEqual('warn'); - }); + test(`should set the theme`, () => { + hostComponent.theme = 'warn'; + fixture.detectChanges(); + expect(component.theme).toEqual('warn'); }); - describe(`ratioConstraint format`, () => { - test(`should throw error if ratioContraint is not in right format`, () => { - expect(() => { - try { - hostComponent.ratioConstraints = '5' as any; - fixture.detectChanges(); - } catch (e) { - throw new Error(e); - } - }).toThrowError(); - }); + }); + + describe(`ratioConstraint format`, () => { + test(`should throw error if ratioContraint is not in right format`, () => { + expect(() => { + try { + hostComponent.ratioConstraints = '5' 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 097ff8265..bf3b67fe2 100644 --- a/terminus-ui/file-upload/src/file-upload.component.ts +++ b/terminus-ui/file-upload/src/file-upload.component.ts @@ -30,6 +30,7 @@ import { coerceArray, coerceNumberProperty, } from '@terminus/ngx-tools/coercion'; +import { KEYS } from '@terminus/ngx-tools/keycodes'; import { TS_SPACING } from '@terminus/ui/spacing'; import { ControlValueAccessorProviderFactory, @@ -41,10 +42,11 @@ import { } from '@terminus/ui/utilities'; import { filter } from 'rxjs/operators'; -import { KEYS } from '@terminus/ngx-tools/keycodes'; import { TsDropProtectionService } from './drop-protection.service'; import { TsFileImageDimensionConstraints } from './image-dimension-constraints'; -import { TS_ACCEPTED_MIME_TYPES, TsFileAcceptedMimeTypes } from './mime-types'; +import { + TS_ACCEPTED_MIME_TYPES, TsFileAcceptedMimeTypes, +} from './mime-types'; import { TsSelectedFile } from './selected-file'; @@ -58,6 +60,7 @@ export interface ImageRatio { * * NOTE: Currently nginx has a hard limit of 10mb */ +// eslint-disable-next-line no-magic-numbers const MAXIMUM_KILOBYTES_PER_FILE = 10 * 1024; /** @@ -106,15 +109,15 @@ let nextUniqueId = 0; templateUrl: './file-upload.component.html', styleUrls: ['./file-upload.component.scss'], host: { - class: 'ts-file-upload', + 'class': 'ts-file-upload', '(keydown)': 'handleKeydown($event)', }, - providers: [ControlValueAccessorProviderFactory(TsFileUploadComponent)], + providers: [ControlValueAccessorProviderFactory(TsFileUploadComponent)], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, exportAs: 'tsFileUpload', }) -export class TsFileUploadComponent extends TsReactiveFormBaseComponent implements OnInit , OnChanges, OnDestroy, AfterContentInit { +export class TsFileUploadComponent extends TsReactiveFormBaseComponent implements OnInit, OnChanges, OnDestroy, AfterContentInit { /** * Define the default component ID */ @@ -158,9 +161,9 @@ export class TsFileUploadComponent extends TsReactiveFormBaseComponent implement public get buttonMessage(): string { if (this.dragInProgress) { return `Drop File${this.multiple ? 's' : ''}`; - } else { - return `Select File${this.multiple ? 's' : ''}`; } + return `Select File${this.multiple ? 's' : ''}`; + } /** @@ -170,13 +173,11 @@ export class TsFileUploadComponent extends TsReactiveFormBaseComponent implement */ public get hints(): string[] { const hints: string[] = []; - const types: string = this.acceptedTypes.slice().map((v) => { - return v.split('/')[1]; - }).join(', '); - const allowsImage = - (this.acceptedTypes.indexOf('image/png') >= 0) || - (this.acceptedTypes.indexOf('image/jpeg') >= 0) || - (this.acceptedTypes.indexOf('image/jpg') >= 0); + const types: string = this.acceptedTypes.slice().map(v => v.split('/')[1]).join(', '); + const allowsImage + = (this.acceptedTypes.indexOf('image/png') >= 0) + || (this.acceptedTypes.indexOf('image/jpeg') >= 0) + || (this.acceptedTypes.indexOf('image/jpg') >= 0); if (allowsImage && this.supportedImageDimensions.length > 0) { hints.push(`Must be a valid dimension: ${this.supportedImageDimensions}`); @@ -234,10 +235,10 @@ export class TsFileUploadComponent extends TsReactiveFormBaseComponent implement */ @Input() public set accept(value: TsFileAcceptedMimeTypes | TsFileAcceptedMimeTypes[] | undefined) { - if (!value) { - this._acceptedTypes = TS_ACCEPTED_MIME_TYPES.slice(); - } else { + if (value) { this._acceptedTypes = coerceArray(value); + } else { + this._acceptedTypes = TS_ACCEPTED_MIME_TYPES.slice(); } } // NOTE: Setter name is different to allow different types passed in vs returned @@ -297,14 +298,14 @@ export class TsFileUploadComponent extends TsReactiveFormBaseComponent implement /** * Define supported ratio for images */ - @Input() public set ratioConstraints(values: Array | undefined) { if (values) { for (const value of values) { const v = value.split(':'); - if ((v.length !== 2) || (!isNumber(v[0]) && !isNumber(v[1]))) { - throw new Error('An array of image ratio should be as ["1:2", "3:4"]'); + const minPartsForValidRatio = 2; + 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"]'); } } } @@ -332,7 +333,7 @@ export class TsFileUploadComponent extends TsReactiveFormBaseComponent implement public get progress(): number { return this._progress; } - private _progress: number = 0; + private _progress = 0; /** * Seed an existing file (used for multiple upload hack) @@ -353,7 +354,7 @@ export class TsFileUploadComponent extends TsReactiveFormBaseComponent implement newFile.fileLoaded$.pipe( filter((t: TsSelectedFile | undefined): t is TsSelectedFile => t !== undefined), untilComponentDestroyed(this), - ).subscribe((f) => { + ).subscribe(f => { this.formControl.setValue(f.file); this.selected.emit(f); this.setUpNewFile(f); @@ -388,31 +389,31 @@ export class TsFileUploadComponent extends TsReactiveFormBaseComponent implement * Event emitted when the user's cursor enters the field while dragging a file */ @Output() - public enter: EventEmitter = new EventEmitter(); + public readonly enter: EventEmitter = new EventEmitter(); /** * Event emitted when the user's cursor exits the field while dragging a file */ @Output() - public exit: EventEmitter = new EventEmitter(); + public readonly exit: EventEmitter = new EventEmitter(); /** * Event emitted when the user drops or selects a file */ @Output() - public selected: EventEmitter = new EventEmitter(); + public readonly selected: EventEmitter = new EventEmitter(); /** * Event emitted when the user drops or selects multiple files */ @Output() - public selectedMultiple: EventEmitter = new EventEmitter(); + public readonly selectedMultiple: EventEmitter = new EventEmitter(); /** * Event emitted when the user clears a loaded file */ @Output() - public cleared: EventEmitter = new EventEmitter(); + public readonly cleared: EventEmitter = new EventEmitter(); /** * HostListeners @@ -444,7 +445,7 @@ export class TsFileUploadComponent extends TsReactiveFormBaseComponent implement } - constructor( + public constructor( private documentService: TsDocumentService, private elementRef: ElementRef, private changeDetectorRef: ChangeDetectorRef, @@ -467,6 +468,7 @@ export class TsFileUploadComponent extends TsReactiveFormBaseComponent implement // NOTE: This `if` is to avoid: `Error: ViewDestroyedError: Attempt to use a destroyed view: detectChanges` // istanbul ignore else + // eslint-disable-next-line dot-notation if (!this.changeDetectorRef['destroyed']) { this.changeDetectorRef.detectChanges(); } @@ -484,6 +486,7 @@ export class TsFileUploadComponent extends TsReactiveFormBaseComponent implement ).subscribe(() => { // NOTE: This `if` is to avoid: `Error: ViewDestroyedError: Attempt to use a destroyed view: detectChanges` // istanbul ignore else + // eslint-disable-next-line dot-notation if (!this.changeDetectorRef['destroyed']) { this.changeDetectorRef.detectChanges(); } @@ -614,12 +617,13 @@ export class TsFileUploadComponent extends TsReactiveFormBaseComponent implement this.dimensionConstraints, this.acceptedTypes, this.maximumKilobytesPerFile, - this._ratioConstraints); + this._ratioConstraints + ); newFile.fileLoaded$.pipe( filter((t: TsSelectedFile | undefined): t is TsSelectedFile => !!t), untilComponentDestroyed(this), - ).subscribe((f) => { + ).subscribe(f => { this.formControl.setValue(f.file); this.selected.emit(f); this.setUpNewFile(f); @@ -769,10 +773,10 @@ export class TsFileUploadComponent extends TsReactiveFormBaseComponent implement */ private parseRatioStringToObject(ratios: Array | undefined): Array | undefined { if (!ratios) { - return; + return undefined; } const parsedImageRatio: Array = []; - ratios.map((r) => parsedImageRatio.push({ + ratios.map(r => parsedImageRatio.push({ widthRatio: Number(r.split(':')[0]), heightRatio: Number(r.split(':')[1]), })); @@ -787,11 +791,22 @@ export class TsFileUploadComponent extends TsReactiveFormBaseComponent implement private parseRatioToString(ratios: Array | undefined): Array | undefined { if (!ratios) { - return; + return undefined; } const parsedRatio: Array = []; - ratios.map((r) => parsedRatio.push(r.widthRatio.toString() + ':' + r.heightRatio.toString())); + ratios.map(r => parsedRatio.push(`${r.widthRatio.toString() }:${ r.heightRatio.toString()}`)); return parsedRatio; } + + /** + * Function for tracking for-loops changes + * + * @param index - The item index + * @return The unique ID + */ + public trackByFn(index): number { + return index; + } + } diff --git a/terminus-ui/file-upload/src/selected-file.spec.ts b/terminus-ui/file-upload/src/selected-file.spec.ts index 58d2bcc95..9d48f12d3 100644 --- a/terminus-ui/file-upload/src/selected-file.spec.ts +++ b/terminus-ui/file-upload/src/selected-file.spec.ts @@ -29,10 +29,11 @@ const CONSTRAINTS_MOCK: TsFileImageDimensionConstraints = [ // IMAGE MOCK const FILE_BLOB = new Blob( - // tslint:disable: max-line-length + // eslint-disable-next-line max-len ['data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABIAQMAAABvIyEEAAAAA1BMVEXXbFn0Q9OUAAAADklEQVR4AWMYRmAUjAIAAtAAAaW+yXMAAAAASUVORK5CYII='], - // tslint:enable: max-line-length - { type: 'image/png' }, + { + type: 'image/png', + }, ); FILE_BLOB['lastModifiedDate'] = new Date(); FILE_BLOB['name'] = 'foo'; @@ -42,7 +43,9 @@ const FILE_MOCK = FILE_BLOB as File; // CSV MOCK const FILE_CSV_BLOB = new Blob( ['my csv value'], - { type: 'text/csv' }, + { + type: 'text/csv', + }, ); FILE_CSV_BLOB['lastModifiedDate'] = new Date(); FILE_CSV_BLOB['name'] = 'myCSV'; @@ -52,7 +55,9 @@ const FILE_CSV_MOCK = FILE_CSV_BLOB as File; // VIDEO MOCK const FILE_VIDEO_BLOB = new Blob( ['my video value'], - { type: 'video/mp4' }, + { + type: 'video/mp4', + }, ); FILE_VIDEO_BLOB['lastModifiedDate'] = new Date(); FILE_VIDEO_BLOB['name'] = 'myVideo'; @@ -63,47 +68,56 @@ const FILE_VIDEO_MOCK = FILE_VIDEO_BLOB as File; describe(`TsSelectedFile`, function() { - let newSelectedFile: TsSelectedFile; const createFile = ( file = FILE_MOCK, constraints: TsFileImageDimensionConstraints | undefined = CONSTRAINTS_MOCK, types: TsFileAcceptedMimeTypes[] = ['image/png', 'image/jpg'], size = (10 * 1024), - ratio = [{ widthRatio: 1, heightRatio: 1 }], - ): TsSelectedFile => { - return newSelectedFile = new TsSelectedFile( - file, - constraints ? constraints : undefined, - types, - size, - ratio, - ); - }; + ratio = [{ + widthRatio: 1, + heightRatio: 1, + }], + ): TsSelectedFile => new TsSelectedFile( + file, + constraints ? constraints : undefined, + types, + size, + ratio, + ); // Mock `FileReader` and `Image`: beforeEach(() => { // Mock FileReader class DummyFileReader { - onload = jest.fn(); - addEventListener = jest.fn(); - readAsDataURL = jest.fn().mockImplementation(function(this: FileReader) { this.onload({} as Event); }); - // tslint:disable: max-line-length - result = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABIAQMAAABvIyEEAAAAA1BMVEXXbFn0Q9OUAAAADklEQVR4AWMYRmAUjAIAAtAAAaW+yXMAAAAASUVORK5CYII='; - // tslint:enable: max-line-length + public onload = jest.fn(); + public addEventListener = jest.fn(); + public readAsDataURL = jest.fn().mockImplementation(function(this: FileReader) { + this.onload({} as Event); + }); + // eslint-disable-next-line max-len + public result = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABIAQMAAABvIyEEAAAAA1BMVEXXbFn0Q9OUAAAADklEQVR4AWMYRmAUjAIAAtAAAaW+yXMAAAAASUVORK5CYII='; } // Not sure why any is needed (window as any).FileReader = jest.fn(() => new DummyFileReader); class DummyImage { - _onload = () => {}; - set onload(fn) { this._onload = fn; } - get onload() { return this._onload; } - set src(source) { + public _onload = () => {}; + public set onload(fn) { + this._onload = fn; + } + public get onload() { + return this._onload; + } + public set src(source) { this.onload(); } - get naturalWidth() { return 100; } - get naturalHeight() { return 100; } + public get naturalWidth() { + return 100; + } + public get naturalHeight() { + return 100; + } } (window as any).Image = jest.fn(() => new DummyImage()); @@ -114,7 +128,7 @@ describe(`TsSelectedFile`, function() { test(`should set top-level items and validations`, () => { const file = createFile(); - file.fileLoaded$.subscribe((f) => { + file.fileLoaded$.subscribe(f => { if (f) { expect(f.mimeType).toEqual('image/png'); expect(f.size).toEqual(3); @@ -128,9 +142,9 @@ describe(`TsSelectedFile`, function() { }); - test(`should set top-level items and validations for videos`, (done) => { + test(`should set top-level items and validations for videos`, done => { const file = createFile(FILE_VIDEO_MOCK, undefined, ['video/mp4']); - file.fileLoaded$.subscribe((f) => { + file.fileLoaded$.subscribe(f => { if (f) { expect(f.mimeType).toEqual('video/mp4'); expect(f.size).toEqual(3); @@ -177,7 +191,7 @@ describe(`TsSelectedFile`, function() { test(`should return true is the file is a CSV`, () => { const file = createFile(FILE_CSV_MOCK); - file.fileLoaded$.subscribe((f) => { + file.fileLoaded$.subscribe(f => { if (f) { expect(f.isCSV).toEqual(true); } @@ -192,7 +206,7 @@ describe(`TsSelectedFile`, function() { test(`should return true is the file is an image`, () => { const file = createFile(); - file.fileLoaded$.subscribe((f) => { + file.fileLoaded$.subscribe(f => { if (f) { expect(file.isImage).toEqual(true); } @@ -206,7 +220,7 @@ describe(`TsSelectedFile`, function() { test(`should return true is the file is a video`, () => { const file = createFile(FILE_VIDEO_MOCK); - file.fileLoaded$.subscribe((f) => { + file.fileLoaded$.subscribe(f => { if (f) { expect(f.isVideo).toEqual(true); } @@ -221,7 +235,7 @@ describe(`TsSelectedFile`, function() { test(`should return the FileReader result`, () => { const file = createFile(); - file.fileLoaded$.subscribe((f) => { + file.fileLoaded$.subscribe(f => { if (f) { expect(f.fileContents.indexOf('data:image')).toBeGreaterThanOrEqual(0); } @@ -246,15 +260,17 @@ describe(`TsSelectedFile`, function() { window.console.warn = jest.fn(); // Mock FileReader for ArrayBuffer class DummyArrayBufferFileReader { - onload = jest.fn(); - addEventListener = jest.fn(); - readAsDataURL = jest.fn().mockImplementation(function(this: FileReader) { this.onload({} as Event); }); - result = str2ab(FILE_BLOB); + public onload = jest.fn(); + public addEventListener = jest.fn(); + public readAsDataURL = jest.fn().mockImplementation(function(this: FileReader) { + this.onload({} as Event); + }); + public result = str2ab(FILE_BLOB); } (window as any).FileReader = jest.fn(() => new DummyArrayBufferFileReader); const file = createFile(); - file.fileLoaded$.subscribe((f) => {}); + file.fileLoaded$.subscribe(f => {}); expect(window.console.warn).toHaveBeenCalled(); expect.assertions(1); @@ -264,15 +280,17 @@ describe(`TsSelectedFile`, function() { window.console.warn = jest.fn(); // Mock FileReader for ArrayBuffer class DummyArrayBufferFileReader { - onload = jest.fn(); - addEventListener = jest.fn(); - readAsDataURL = jest.fn().mockImplementation(function(this: FileReader) { this.onload({} as Event); }); - result = str2ab(FILE_CSV_BLOB); + public onload = jest.fn(); + public addEventListener = jest.fn(); + public readAsDataURL = jest.fn().mockImplementation(function(this: FileReader) { + this.onload({} as Event); + }); + public result = str2ab(FILE_CSV_BLOB); } (window as any).FileReader = jest.fn(() => new DummyArrayBufferFileReader); const file = createFile(FILE_CSV_MOCK); - file.fileLoaded$.subscribe((f) => { + file.fileLoaded$.subscribe(f => { file.fileContents.valueOf(); }); expect(window.console.warn).toHaveBeenCalled(); @@ -325,9 +343,9 @@ describe(`TsSelectedFile`, function() { describe(`determineImageDimensions`, () => { - test(`should set validation to true and exit if the file is not an image`, (done) => { + test(`should set validation to true and exit if the file is not an image`, done => { const file = createFile(FILE_CSV_MOCK); - file.fileLoaded$.subscribe((f) => { + file.fileLoaded$.subscribe(f => { if (f) { expect(f.dimensions).toBeFalsy(); expect(f.height).toEqual(0); @@ -340,9 +358,9 @@ describe(`TsSelectedFile`, function() { }); - test(`should still seed the FileReader for non-image files`, (done) => { + test(`should still seed the FileReader for non-image files`, done => { const file = createFile(FILE_CSV_MOCK); - file.fileLoaded$.subscribe((f) => { + file.fileLoaded$.subscribe(f => { if (f) { expect(f.fileContents).toBeTruthy(); done(); @@ -353,7 +371,7 @@ describe(`TsSelectedFile`, function() { test(`should set dimensions and call callback`, () => { - createFile().fileLoaded$.subscribe((f) => { + createFile().fileLoaded$.subscribe(f => { if (f) { expect(f.dimensions).toBeTruthy(); expect(f.height).toBeGreaterThan(1); @@ -377,14 +395,20 @@ describe(`TsSelectedFile`, function() { test(`should return true if ratio are valid`, () => { const file = createFile(); - const result = file['validateImageRatio']([{widthRatio: 1, heightRatio: 1}]); + 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']([{ widthRatio: 2, heightRatio: 1 }]); + const result = file['validateImageRatio']([{ + widthRatio: 2, + heightRatio: 1, + }]); 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 5d0f18459..17f9c336f 100644 --- a/terminus-ui/file-upload/src/selected-file.ts +++ b/terminus-ui/file-upload/src/selected-file.ts @@ -1,11 +1,14 @@ import { isDevMode } from '@angular/core'; +import { isString } from '@terminus/ngx-tools'; import { BehaviorSubject } from 'rxjs'; -import { isString } from '@terminus/ngx-tools'; import { ImageRatio } from './file-upload.module'; import { TsFileImageDimensionConstraints } from './image-dimension-constraints'; import { TsImageDimensions } from './image-dimensions'; -import { TS_ACCEPTED_MIME_TYPES, TsFileAcceptedMimeTypes } from './mime-types'; +import { + TS_ACCEPTED_MIME_TYPES, + TsFileAcceptedMimeTypes, +} from './mime-types'; /** @@ -142,13 +145,11 @@ export class TsSelectedFile { */ public get fileContents(): string { if (isString(this.fileReader.result)) { - return this.fileReader.result; - } else { - // istanbul ignore else - if (isDevMode) { - console.warn(`${this.fileReader.result} is not returning a string.`); - } + return this.fileReader.result; + } else if (isDevMode) { + console.warn(`${this.fileReader.result} is not returning a string.`); } + return ''; } /** @@ -178,11 +179,8 @@ export class TsSelectedFile { if (img) { if (isString(this.fileReader.result)) { img.src = this.fileReader.result; - } else { - // istanbul ignore else - if (isDevMode) { - console.warn(`${img} is not returning a string.`); - } + } else if (isDevMode) { + console.warn(`${img} is not returning a string.`); } } }; @@ -255,7 +253,7 @@ export class TsSelectedFile { return true; } - const ratios = constraints.map((r) => r.widthRatio / r.heightRatio); + const ratios = constraints.map(r => r.widthRatio / r.heightRatio); for (const r of ratios) { const ratio = this.width / this.height; if (this.isSame(r, ratio)) { @@ -268,17 +266,17 @@ export class TsSelectedFile { /** * A utility function to determine whether two numbers are the same + * * @param number1 - one number * @param number2 - another number * @return Whether these two numbers are the same */ - - private isSame(number1: number, number2: number) { - if (Math.abs((number1 - number2) / number1) < 0.001) { + private isSame(number1: number, number2: number): boolean { + const minimumAmountToConsiderMatch = .001; + if (Math.abs((number1 - number2) / number1) < minimumAmountToConsiderMatch) { return true; - } else { - return false; } + return false; } } @@ -292,8 +290,6 @@ export class TsSelectedFile { */ function typeNeedsDimensionValidation(type: TsFileAcceptedMimeTypes): boolean { const allTypes = TS_ACCEPTED_MIME_TYPES.slice(); - const itemsNeedingValidation = allTypes.filter((item) => { - return !typesWithoutDimensionValidation.includes(item); - }); + const itemsNeedingValidation = allTypes.filter(item => !typesWithoutDimensionValidation.includes(item)); return itemsNeedingValidation.indexOf(type) >= 0; } diff --git a/terminus-ui/form-field/src/form-field-control.ts b/terminus-ui/form-field/src/form-field-control.ts index ff35aca1c..68c8b97d8 100644 --- a/terminus-ui/form-field/src/form-field-control.ts +++ b/terminus-ui/form-field/src/form-field-control.ts @@ -1,4 +1,7 @@ -import { FormControl, NgControl } from '@angular/forms'; +import { + FormControl, + NgControl, +} from '@angular/forms'; import { Observable } from 'rxjs'; @@ -15,61 +18,61 @@ export abstract class TsFormFieldControl { * Stream that emits whenever the state of the control changes such that the * parent {@link TsFormFieldComponent} needs to run change detection. */ - readonly stateChanges!: Observable; + public readonly stateChanges!: Observable; /** * Stream that emits whenever the label of the control changes such that the * parent {@link TsFormFieldComponent} needs to update its outline gap. */ - readonly labelChanges!: Observable; + public readonly labelChanges!: Observable; /** * The element ID for this control */ - readonly id!: string; + public readonly id!: string; /** * The NgControl for this control */ - readonly ngControl!: NgControl | null; + public readonly ngControl!: NgControl | null; /** * Whether the control is focused */ - readonly focused!: boolean; + public readonly focused!: boolean; /** * Whether the control is empty */ - readonly empty!: boolean; + public readonly empty!: boolean; /** * Whether the `TsFormFieldComponent` label should try to float */ - readonly shouldLabelFloat!: boolean; + public readonly shouldLabelFloat!: boolean; /** * Whether the control is required */ - readonly isRequired!: boolean; + public readonly isRequired!: boolean; /** * Whether the control is disabled */ - readonly isDisabled!: boolean; + public readonly isDisabled!: boolean; /** * Whether the input is currently in an autofilled state. If property is not present on the control it is assumed to be false. */ - readonly autofilled?: boolean; + public readonly autofilled?: boolean; /** * An optional form control (used by DateRange) */ - readonly formControl?: FormControl; + public readonly formControl?: FormControl; /** * Handles a click on the control's container */ - abstract onContainerClick(event: MouseEvent): void; + public abstract onContainerClick(event: MouseEvent): void; } diff --git a/terminus-ui/form-field/src/form-field.component.spec.ts b/terminus-ui/form-field/src/form-field.component.spec.ts index 07c2d9f31..c1a0f1a52 100644 --- a/terminus-ui/form-field/src/form-field.component.spec.ts +++ b/terminus-ui/form-field/src/form-field.component.spec.ts @@ -5,9 +5,7 @@ import { Type, ViewChild, } from '@angular/core'; -import { - ComponentFixture, -} from '@angular/core/testing'; +import { ComponentFixture } from '@angular/core/testing'; import { FormControl, FormsModule, @@ -22,9 +20,15 @@ import { createFakeEvent, TsDocumentServiceMock, } from '@terminus/ngx-tools/testing'; -import { TsInputComponent, TsInputModule } from '@terminus/ui/input'; +import { + TsInputComponent, + TsInputModule, +} from '@terminus/ui/input'; -import { TsFormFieldComponent, TsFormFieldModule } from './form-field.module'; +import { + TsFormFieldComponent, + TsFormFieldModule, +} from './form-field.module'; // tslint:disable: no-use-before-declare @@ -182,7 +186,9 @@ describe(`TsFormFieldComponent`, function() { expect(formField.shouldAlwaysFloat).toBe(false); expect(formField.floatLabel).toBe('always'); - const fakeEvent = (Object as any).assign(createFakeEvent('transitionend'), {propertyName: 'transform'}); + const fakeEvent = (Object as any).assign(createFakeEvent('transitionend'), { + propertyName: 'transform', + }); label.dispatchEvent(fakeEvent); fixture.detectChanges(); diff --git a/terminus-ui/form-field/src/form-field.component.ts b/terminus-ui/form-field/src/form-field.component.ts index 853c61007..282848f5b 100644 --- a/terminus-ui/form-field/src/form-field.component.ts +++ b/terminus-ui/form-field/src/form-field.component.ts @@ -73,7 +73,7 @@ const OUTLINE_GAP_PADDING = 5; templateUrl: './form-field.component.html', styleUrls: ['./form-field.component.scss'], host: { - class: 'ts-form-field', + 'class': 'ts-form-field', '[class.ts-form-field--invalid]': 'controlIsInErrorState', '[class.ts-form-field--float]': 'shouldLabelFloat()', '[class.ts-form-field--disabled]': 'control.isDisabled', @@ -185,6 +185,7 @@ export class TsFormFieldComponent implements AfterContentInit, AfterContentCheck * NOTE: Using non-null-assertion as since the existence is verified by `confirmControlExists()` */ @Input() + // tslint:disable-next-line no-any public control!: TsFormFieldControl; /** @@ -282,7 +283,7 @@ export class TsFormFieldComponent implements AfterContentInit, AfterContentCheck } // Run change detection if the value, prefix, or suffix changes. - const valueChanges = this.control.ngControl && this.control.ngControl.valueChanges || EMPTY; + const valueChanges = (this.control.ngControl && this.control.ngControl.valueChanges) || EMPTY; merge(valueChanges, this.prefixChildren.changes, this.suffixChildren.changes).subscribe(() => this.changeDetectorRef.markForCheck()); this.ngZone.onStable.pipe(take(1)).subscribe(() => { @@ -406,12 +407,14 @@ export class TsFormFieldComponent implements AfterContentInit, AfterContentCheck } startWidth = labelStart - containerStart - OUTLINE_GAP_PADDING; - gapWidth = (labelWidth > 0) ? labelWidth * FLOATING_LABEL_SCALE + OUTLINE_GAP_PADDING * 2 : 0; + const TWO = 2; + gapWidth = (labelWidth > 0) ? (labelWidth * FLOATING_LABEL_SCALE) + (OUTLINE_GAP_PADDING * TWO) : 0; } for (let i = 0; i < startEls.length; i++) { startEls.item(i).style.width = `${startWidth}px`; } + for (let i = 0; i < gapEls.length; i++) { gapEls.item(i).style.width = `${gapWidth}px`; } diff --git a/terminus-ui/icon-button/src/icon-button.component.spec.ts b/terminus-ui/icon-button/src/icon-button.component.spec.ts index c3cf67cd2..a0ad598d3 100644 --- a/terminus-ui/icon-button/src/icon-button.component.spec.ts +++ b/terminus-ui/icon-button/src/icon-button.component.spec.ts @@ -1,4 +1,7 @@ -import { Component, ViewChild } from '@angular/core'; +import { + Component, + ViewChild, +} from '@angular/core'; import { ComponentFixture } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; diff --git a/terminus-ui/icon-button/src/icon-button.component.ts b/terminus-ui/icon-button/src/icon-button.component.ts index d5266bf94..b68a6ac34 100644 --- a/terminus-ui/icon-button/src/icon-button.component.ts +++ b/terminus-ui/icon-button/src/icon-button.component.ts @@ -70,7 +70,7 @@ export class TsIconButtonComponent { * Pass the click event through to the parent */ @Output() - public clicked: EventEmitter = new EventEmitter(); + public readonly clicked: EventEmitter = new EventEmitter(); /** diff --git a/terminus-ui/icon/src/custom-icons/csv.ts b/terminus-ui/icon/src/custom-icons/csv.ts index 7a987ba76..7ca8ae6a4 100644 --- a/terminus-ui/icon/src/custom-icons/csv.ts +++ b/terminus-ui/icon/src/custom-icons/csv.ts @@ -1,4 +1,4 @@ -// tslint:disable: max-line-length +/* eslint-disable max-len */ export const CSV_ICON = ` diff --git a/terminus-ui/icon/src/custom-icons/engage.ts b/terminus-ui/icon/src/custom-icons/engage.ts index 2b9c39727..2560b5775 100644 --- a/terminus-ui/icon/src/custom-icons/engage.ts +++ b/terminus-ui/icon/src/custom-icons/engage.ts @@ -1,4 +1,4 @@ -// tslint:disable: max-line-length +/* eslint-disable max-len */ export const ENGAGE_ICON = ` diff --git a/terminus-ui/icon/src/custom-icons/lightbulb.ts b/terminus-ui/icon/src/custom-icons/lightbulb.ts index 4b99ff0b0..2d6ed49a5 100644 --- a/terminus-ui/icon/src/custom-icons/lightbulb.ts +++ b/terminus-ui/icon/src/custom-icons/lightbulb.ts @@ -1,4 +1,4 @@ -// tslint:disable: max-line-length +/* eslint-disable max-len */ export const LIGHTBULB_ICON = ` diff --git a/terminus-ui/icon/src/custom-icons/logo.ts b/terminus-ui/icon/src/custom-icons/logo.ts index 6fb409ab1..4200d4051 100644 --- a/terminus-ui/icon/src/custom-icons/logo.ts +++ b/terminus-ui/icon/src/custom-icons/logo.ts @@ -1,4 +1,4 @@ -// tslint:disable: max-line-length +/* eslint-disable max-len */ export const LOGO_ICON = ` diff --git a/terminus-ui/icon/src/custom-icons/logo_color.ts b/terminus-ui/icon/src/custom-icons/logo_color.ts index cbbdfc8f6..c6c54dde8 100644 --- a/terminus-ui/icon/src/custom-icons/logo_color.ts +++ b/terminus-ui/icon/src/custom-icons/logo_color.ts @@ -1,4 +1,4 @@ -// tslint:disable: max-line-length +/* eslint-disable max-len */ export const LOGO_COLOR_ICON = ` diff --git a/terminus-ui/icon/src/icon.component.spec.ts b/terminus-ui/icon/src/icon.component.spec.ts index 30eca3af6..d20ed2d95 100644 --- a/terminus-ui/icon/src/icon.component.spec.ts +++ b/terminus-ui/icon/src/icon.component.spec.ts @@ -1,4 +1,7 @@ -import { Component, ViewChild } from '@angular/core'; +import { + Component, + ViewChild, +} from '@angular/core'; import { ComponentFixture, TestBed, diff --git a/terminus-ui/icon/src/icon.component.ts b/terminus-ui/icon/src/icon.component.ts index a9d2c7399..ca49532a6 100644 --- a/terminus-ui/icon/src/icon.component.ts +++ b/terminus-ui/icon/src/icon.component.ts @@ -58,7 +58,7 @@ export const TS_CUSTOM_ICONS: TS_CUSTOM_ICON[] = [ templateUrl: './icon.component.html', styleUrls: ['./icon.component.scss'], host: { - class: 'ts-icon', + 'class': 'ts-icon', '[class.ts-icon--inline]': 'inline', '[class.ts-icon--primary]': 'theme === "primary"', '[class.ts-icon--accent]': 'theme === "accent"', @@ -91,8 +91,8 @@ export class TsIconComponent { public set svgIcon(value: TS_CUSTOM_ICON | undefined) { // If an unsupported value is passed in if (value && TS_CUSTOM_ICONS.indexOf(value) < 0 && isDevMode()) { - console.warn(`TsIconComponent: "${value}" is not a supported custom icon. ` + - `See TS_CUSTOM_ICON for available options.`); + console.warn(`TsIconComponent: "${value}" is not a supported custom icon. ` + + `See TS_CUSTOM_ICON for available options.`); return; } diff --git a/terminus-ui/input/src/date-adapter.ts b/terminus-ui/input/src/date-adapter.ts index c41491a78..d50552164 100644 --- a/terminus-ui/input/src/date-adapter.ts +++ b/terminus-ui/input/src/date-adapter.ts @@ -42,9 +42,10 @@ export class TsDateAdapter extends NativeDateAdapter { * Format the date when setting the UI * * @param date - The date chosen - * @param date - The desired format (not currently using, but must match API) + * @param displayFormat - The desired format (not currently using, but must match API) * @return The date string */ + // tslint:disable-next-line no-any public format(date: Date, displayFormat?: any): string { const day = this.forceTwoDigits(date.getDate()); const month = this.forceTwoDigits(date.getMonth() + 1); @@ -74,6 +75,7 @@ export class TsDateAdapter extends NativeDateAdapter { * @return The two digit number */ private forceTwoDigits(n: number): string { - return ('00' + n).slice(-2); + const digitsToRemove = -2; + return (`00${ n.toString()}`).slice(digitsToRemove); } } diff --git a/terminus-ui/input/src/input-value-accessor.ts b/terminus-ui/input/src/input-value-accessor.ts index 20a32c4e9..f9878045b 100644 --- a/terminus-ui/input/src/input-value-accessor.ts +++ b/terminus-ui/input/src/input-value-accessor.ts @@ -7,4 +7,5 @@ import { InjectionToken } from '@angular/core'; * themselves for this token, in order to make `TsInputComponent` delegate the getting and setting of the * value to them. */ +// tslint:disable-next-line no-any export const TS_INPUT_VALUE_ACCESSOR = new InjectionToken<{value: any}>('TS_INPUT_VALUE_ACCESSOR'); diff --git a/terminus-ui/input/src/input.component.spec.ts b/terminus-ui/input/src/input.component.spec.ts index 4e77ddfb7..9fa1226d1 100644 --- a/terminus-ui/input/src/input.component.spec.ts +++ b/terminus-ui/input/src/input.component.spec.ts @@ -24,10 +24,17 @@ import { TsDocumentServiceMock, typeInElement, } from '@terminus/ngx-tools/testing'; -import { TsFormFieldComponent, TsFormFieldModule } from '@terminus/ui/form-field'; -import { Observable, Subject } from 'rxjs'; +import { + TsFormFieldComponent, + TsFormFieldModule, +} from '@terminus/ui/form-field'; +import { + Observable, + Subject, +} from 'rxjs'; import * as TestComponents from '@terminus/ui/input/testing'; +// eslint-disable-next-line no-duplicate-imports import { getInputElement, getInputInstance, @@ -631,13 +638,19 @@ describe(`TsInputComponent`, function() { const outlineStartEl: HTMLDivElement = fixture.debugElement.query(By.css('.js-outline-start')).nativeElement; const outlineGapEl: HTMLDivElement = fixture.debugElement.query(By.css('.js-outline-gap')).nativeElement; const labelContent: HTMLSpanElement = fixture.debugElement.query(By.css('.c-input__label-text')).nativeElement; - const bounding1 = { left: 50 }; - const bounding2 = { left: 100 }; + const bounding1 = { + left: 50, + }; + const bounding2 = { + left: 100, + }; const formFieldInstance: TsFormFieldComponent = fixture.debugElement.query(By.css('.ts-form-field')).componentInstance; formFieldInstance['containerElement'].nativeElement.getBoundingClientRect = jest.fn(() => bounding1); formFieldInstance['labelElement'].nativeElement.children[0].getBoundingClientRect = jest.fn(() => bounding2); Object.defineProperty(formFieldInstance['labelElement'].nativeElement.children[0], 'offsetWidth', { - get() { return 40; }, + get() { + return 40; + }, }); formFieldInstance['updateOutlineGap'](); @@ -955,14 +968,16 @@ function createComponent(component: Type): ComponentFixture { { provide: AutofillMonitor, useClass: AutofillMonitorMock, - }], + }, + ], [ FormsModule, ReactiveFormsModule, TsFormFieldModule, TsInputModule, NoopAnimationsModule, - ]); + ], + ); } @@ -971,10 +986,10 @@ function createComponent(component: Type): ComponentFixture { */ class MyDocumentService extends TsDocumentServiceMock { - shouldContain = true; - document: any = { + public shouldContain = true; + public document: any = { documentElement: { - contains: jest.fn(() => this.shouldContain ? true : false), + contains: jest.fn(() => !!this.shouldContain), }, createEvent() { return document.createEvent('Event'); @@ -988,19 +1003,22 @@ interface AutofillEvent { isAutofilled: boolean; } class AutofillMonitorMock { - result = new Subject(); - el!: Element; + public result = new Subject(); + public el!: Element; - monitor(el: Element): Observable { + public monitor(el: Element): Observable { this.el = el; return this.result; } - fireMockFillEvent() { - this.result.next({target: this.el as Element, isAutofilled: true}); + public fireMockFillEvent() { + this.result.next({ + target: this.el, + isAutofilled: true, + }); } - stopMonitoring(element: Element) { + public stopMonitoring(element: Element) { this.result.complete(); } } diff --git a/terminus-ui/input/src/input.component.ts b/terminus-ui/input/src/input.component.ts index 5d5c20331..9f29ddcd2 100644 --- a/terminus-ui/input/src/input.component.ts +++ b/terminus-ui/input/src/input.component.ts @@ -39,9 +39,7 @@ import { noop, TsDocumentService, } from '@terminus/ngx-tools'; -import { - coerceNumberProperty, -} from '@terminus/ngx-tools/coercion'; +import { coerceNumberProperty } from '@terminus/ngx-tools/coercion'; import { TsFormFieldControl } from '@terminus/ui/form-field'; import { TsDatePipe } from '@terminus/ui/pipes'; import { TS_SPACING } from '@terminus/ui/spacing'; @@ -49,9 +47,7 @@ import { inputHasChanged, TsStyleThemeTypes, } from '@terminus/ui/utilities'; -import { - isValid as isValidDate, -} from 'date-fns'; +import { isValid as isValidDate } from 'date-fns'; import { Subject } from 'rxjs'; import createAutoCorrectedDatePipe from 'text-mask-addons/dist/createAutoCorrectedDatePipe'; import createNumberMask from 'text-mask-addons/dist/createNumberMask'; @@ -63,6 +59,11 @@ import { } from './date-adapter'; import { TS_INPUT_VALUE_ACCESSOR } from './input-value-accessor'; +export interface TextMaskInputElement { + // tslint:disable-next-line no-any + state: Record; + update: Function; +} /** * Define the function type for date filters. Used by {@link TsInputComponent} @@ -133,7 +134,8 @@ export type TsMaskShortcutOptions | 'percentage' | 'phone' | 'postal' - | 'default' // matches all characters + // matches all characters + | 'default' ; /** @@ -159,6 +161,7 @@ const allowedMaskShorcuts: TsMaskShortcutOptions[] = [ * @param item - The item to check * @return Whether the item is a function */ +// tslint:disable-next-line no-any function isFunction(item: any): item is Function { return !!(item && item.constructor && item.call && item.apply); } @@ -227,7 +230,7 @@ const DEFAULT_TEXTAREA_ROWS = 4; templateUrl: './input.component.html', styleUrls: ['./input.component.scss'], host: { - class: 'ts-input', + 'class': 'ts-input', '[class.ts-input--datepicker]': 'datepicker', }, providers: [ @@ -249,6 +252,7 @@ const DEFAULT_TEXTAREA_ROWS = 4; exportAs: 'tsInput', }) export class TsInputComponent implements + // tslint:disable-next-line no-any TsFormFieldControl, OnInit, AfterViewInit, @@ -276,6 +280,7 @@ export class TsInputComponent implements /** * Define an InputValueAccessor for this component */ + // tslint:disable-next-line no-any private inputValueAccessor: {value: any}; /** @@ -308,7 +313,7 @@ export class TsInputComponent implements /** * Implemented as part of TsFormFieldControl. */ - readonly labelChanges: Subject = new Subject(); + public readonly labelChanges: Subject = new Subject(); /** * Store the last value for comparison @@ -318,6 +323,7 @@ export class TsInputComponent implements /** * Define placeholder for callback (provided later by the control value accessor) */ + // tslint:disable-next-line no-any private onChangeCallback: (_: any) => void = noop; /** @@ -325,7 +331,10 @@ export class TsInputComponent implements */ private onTouchedCallback: () => void = noop; - + /** + * Store the previous value + */ + // tslint:disable-next-line no-any private previousNativeValue: any; /** @@ -336,12 +345,13 @@ export class TsInputComponent implements /** * Implemented as part of TsFormFieldControl. */ - readonly stateChanges: Subject = new Subject(); + public readonly stateChanges: Subject = new Subject(); /** * Base settings for the mask */ - private textMaskConfig: any = { + // tslint:disable-next-line no-any + private textMaskConfig: Record = { mask: null, guide: false, keepCharPositions: false, @@ -350,7 +360,7 @@ export class TsInputComponent implements /** * Store the mask instance */ - private textMaskInputElement: any; + private textMaskInputElement!: TextMaskInputElement; /** * Define the default component ID @@ -412,6 +422,7 @@ export class TsInputComponent implements /** * Set the accessor and call the onchange callback */ + // tslint:disable-next-line no-any public set value(v: any) { const oldDate = this.value; @@ -430,6 +441,7 @@ export class TsInputComponent implements } } } + // tslint:disable-next-line no-any public get value(): any { return this.inputValueAccessor.value; } @@ -509,6 +521,7 @@ export class TsInputComponent implements this.inputValueAccessor.value = this._formControl.value; }); // HACK: This is to get disabled field set properly on both datepicker and input level + // eslint-disable-next-line dot-notation if (!this.changeDetectorRef['destroyed']) { this.changeDetectorRef.detectChanges(); } @@ -630,8 +643,8 @@ export class TsInputComponent implements // Verify value is allowed // istanbul ignore else if (value && isDevMode() && (allowedMaskShorcuts.indexOf(value) < 0)) { - console.warn(`TsInputComponent: "${value}" is not an allowed mask. ` + - 'Allowed masks are defined by "TsMaskShortcutOptions".'); + console.warn(`TsInputComponent: "${value}" is not an allowed mask. ` + + 'Allowed masks are defined by "TsMaskShortcutOptions".'); // Fallback to the default mask (which will allow all characters) value = 'default'; @@ -794,8 +807,8 @@ export class TsInputComponent implements // istanbul ignore else if (this.mask && (value === 'email' || value === 'number')) { - console.warn(`TsInputComponent: "${value}" is not an allowed type when used with a mask. ` + - 'When using a mask, the input type must be "text", "tel", "url", "password" or "search".'); + console.warn(`TsInputComponent: "${value}" is not an allowed type when used with a mask. ` + + 'When using a mask, the input type must be "text", "tel", "url", "password" or "search".'); value = 'text'; } @@ -805,11 +818,8 @@ export class TsInputComponent implements // Update the autocomplete setting if needed if (value === 'email') { this.autocomplete = 'email'; - } else { - // istanbul ignore else - if (this.autocomplete === 'email') { - this.autocomplete = AUTOCOMPLETE_DEFAULT; - } + } else if (this.autocomplete === 'email') { + this.autocomplete = AUTOCOMPLETE_DEFAULT; } } public get type(): TsInputTypes { @@ -832,28 +842,28 @@ export class TsInputComponent implements * The event to emit when the input value is cleared */ @Output() - readonly cleared: EventEmitter = new EventEmitter(); + public readonly cleared: EventEmitter = new EventEmitter(); /** * Define an event when the input receives a blur event */ @Output() - readonly inputBlur: EventEmitter = new EventEmitter(); + public readonly inputBlur: EventEmitter = new EventEmitter(); /** * The event to emit when the input element receives a focus event */ @Output() - readonly inputFocus: EventEmitter = new EventEmitter(); + public readonly inputFocus: EventEmitter = new EventEmitter(); /** * Define an event emitter to alert consumers that a date was selected */ @Output() - readonly selected: EventEmitter = new EventEmitter(); + public readonly selected: EventEmitter = new EventEmitter(); - constructor( + public constructor( private elementRef: ElementRef, private renderer: Renderer2, private changeDetectorRef: ChangeDetectorRef, @@ -862,6 +872,7 @@ export class TsInputComponent implements private ngZone: NgZone, private documentService: TsDocumentService, private datePipe: TsDatePipe, + // tslint:disable-next-line no-any @Optional() @Self() @Inject(TS_INPUT_VALUE_ACCESSOR) inputValueAccessor: any, @Optional() public dateAdapter: DateAdapter, @Optional() @Self() public ngControl: NgControl, @@ -869,7 +880,9 @@ export class TsInputComponent implements this.document = this.documentService.document; // If no inputValueAccessor was passed in, default to a basic object with a value. - this.inputValueAccessor = inputValueAccessor || {value: undefined}; + this.inputValueAccessor = inputValueAccessor || { + value: undefined, + }; // If no value accessor was passed in, use this component for the ngControl ValueAccessor // istanbul ignore else @@ -890,7 +903,7 @@ export class TsInputComponent implements * Begin monitoring for the input autofill */ public ngOnInit(): void { - this.autofillMonitor.monitor(this.elementRef.nativeElement).subscribe((event) => { + this.autofillMonitor.monitor(this.elementRef.nativeElement).subscribe(event => { this.autofilled = event.isAutofilled; this.stateChanges.next(); }); @@ -938,12 +951,19 @@ export class TsInputComponent implements // Register this component as the associated input for the Material datepicker // istanbul ignore else + // NOTE: Dangle naming controlled by Material + // eslint-disable-next-line no-underscore-dangle if (this.picker && !this.picker._datepickerInput) { + // NOTE: Dangle naming controlled by Material + /* eslint-disable no-underscore-dangle */ + // tslint:disable-next-line no-any this.picker._registerInput(this as any); + /* eslint-enable no-underscore-dangle */ } } + // tslint:disable-next-line no-conflicting-lifecycle public ngDoCheck(): void { // We need to dirty-check the native element's value, because there are some cases where we won't be notified when it changes (e.g. the // consumer isn't using forms or they're updating the value using `emitEvent: false`). @@ -1064,6 +1084,7 @@ export class TsInputComponent implements */ public updateInnerValue = (value: string): void => { this.value = value; + // eslint-disable-next-line dot-notation if (!this.changeDetectorRef['destroyed']) { this.changeDetectorRef.detectChanges(); } @@ -1073,6 +1094,7 @@ export class TsInputComponent implements /** * Register onChange callback (from ControlValueAccessor interface) */ + // tslint:disable-next-line no-any public registerOnChange(fn: any): void { this.onChangeCallback = fn; } @@ -1081,6 +1103,7 @@ export class TsInputComponent implements /** * Register onTouched callback (from ControlValueAccessor interface) */ + // tslint:disable-next-line no-any public registerOnTouched(fn: any): void { this.onTouchedCallback = fn; } @@ -1109,12 +1132,12 @@ export class TsInputComponent implements this.stateChanges.next(); } - // Trigger the onTouchedCallback for blur events - if (!nowFocused) { + if (nowFocused) { + this.inputFocus.emit(this.value); + } else { + // Trigger the onTouchedCallback for blur events this.onTouchedCallback(); this.inputBlur.emit(this.value); - } else { - this.inputFocus.emit(this.value); } } @@ -1201,11 +1224,11 @@ export class TsInputComponent implements // If there is no unmask regex, just return the value if (!regex) { return value; - } else { - // If the unmask regex is a function, invoke it to get the plain regex - const finalRegex: RegExp = isFunction(regex) ? regex() : regex; - return finalRegex ? value.replace(new RegExp(finalRegex), '') : value; } + // If the unmask regex is a function, invoke it to get the plain regex + const finalRegex: RegExp = isFunction(regex) ? regex() : regex; + return finalRegex ? value.replace(new RegExp(finalRegex), '') : value; + } @@ -1223,7 +1246,7 @@ export class TsInputComponent implements }, currency: { mask: createNumberMask({ - allowDecimal: allowDecimal, + allowDecimal, }), unmaskRegex: allowDecimal ? NUMBER_WITH_DECIMAL_REGEX : NUMBER_ONLY_REGEX, }, @@ -1231,7 +1254,7 @@ export class TsInputComponent implements mask: createNumberMask({ prefix: '', suffix: '', - allowDecimal: allowDecimal, + allowDecimal, allowLeadingZeroes: true, }), unmaskRegex: allowDecimal ? NUMBER_WITH_DECIMAL_REGEX : NUMBER_ONLY_REGEX, @@ -1240,7 +1263,7 @@ export class TsInputComponent implements mask: createNumberMask({ prefix: '', suffix: '%', - allowDecimal: allowDecimal, + allowDecimal, }), unmaskRegex: allowDecimal ? NUMBER_WITH_DECIMAL_REGEX : NUMBER_ONLY_REGEX, }, @@ -1266,11 +1289,11 @@ export class TsInputComponent implements * @return The correct mask */ private determinePostalMask(value: string): (RegExp | string)[] { - if (!value || value.length <= 5) { + const MIN_POSTAL_CODE_LENGTH = 5; + if (!value || value.length <= MIN_POSTAL_CODE_LENGTH) { return [/\d/, /\d/, /\d/, /\d/, /\d/]; - } else { - return [/\d/, /\d/, /\d/, /\d/, /\d/, '-', /\d/, /\d/, /\d/, /\d/]; } + return [/\d/, /\d/, /\d/, /\d/, /\d/, '-', /\d/, /\d/, /\d/, /\d/]; } @@ -1280,7 +1303,7 @@ export class TsInputComponent implements * @return Whether the native validation passes */ private isBadInput(): boolean { - const validity: ValidityState = (this.inputElement.nativeElement as HTMLInputElement).validity; + const validity: ValidityState = (this.inputElement.nativeElement).validity; return validity && validity.badInput; } @@ -1294,7 +1317,7 @@ export class TsInputComponent implements if (value && this.mask === 'date') { this.onChangeCallback(new Date(value)); } else { - const finalValue = !this.maskSanitizeValue ? value : this.cleanValue(value, this.currentMask.unmaskRegex); + const finalValue = this.maskSanitizeValue ? this.cleanValue(value, this.currentMask.unmaskRegex) : value; this.onChangeCallback(finalValue); } } @@ -1322,12 +1345,15 @@ export class TsInputComponent implements const collection: TsMaskCollection = this.createMaskCollection(this.maskAllowDecimal); // NOTE: If the mask doesn't match a predefined mask, default to a mask that matches all // characters. The underlying text-mask library will error out without this fallback. - const mask = (value && collection[value]) ? collection[value] : collection['default']; + const mask = (value && collection[value]) ? collection[value] : collection.default; // Set the current mask this.currentMask = mask; // Update the config with the chosen mask - this.textMaskConfig = Object.assign({}, this.textMaskConfig, mask); + this.textMaskConfig = { + ...this.textMaskConfig, + ...mask, + }; } @@ -1337,8 +1363,11 @@ export class TsInputComponent implements private setUpMask(): void { // istanbul ignore else if (this.inputElement) { - const maskOptions: {[key: string]: any} = - Object.assign({inputElement: this.inputElement.nativeElement}, this.textMaskConfig); + // tslint:disable-next-line no-any + const maskOptions: {[key: string]: any} = { + inputElement: this.inputElement.nativeElement, + ...this.textMaskConfig, + }; // Initialize the mask this.textMaskInputElement = createTextMaskInputElement(maskOptions); @@ -1379,6 +1408,7 @@ export class TsInputComponent implements const mask = this.currentMask.mask; const staticMask = isFunction(mask) ? mask(this.value) : mask; const maskLength = staticMask ? staticMask.length /* istanbul ignore next - Unreachable */ : 0; + // tslint:disable-next-line no-any const isNumberMask: boolean = (mask as any).instanceOf === 'createNumberMask'; // istanbul ignore else @@ -1386,8 +1416,9 @@ export class TsInputComponent implements const decimals = 2; const cleanValue = this.maskSanitizeValue ? this.cleanValue(value, this.currentMask.unmaskRegex) : value; const split = cleanValue.split('.'); + const twoItems = 2; - if (split.length === 2 && split[1].length > decimals) { + if (split.length === twoItems && split[1].length > decimals) { // Trim the final character off const trimmedValue = cleanValue.slice(0, -1); value = trimmedValue; @@ -1428,7 +1459,7 @@ export class TsInputComponent implements * @return The Date object */ private verifyIsDateObject(date: string | Date): Date { - return !(date instanceof Date) ? new Date(date) : date; + return (date instanceof Date) ? date : new Date(date); } diff --git a/terminus-ui/input/testing/src/test-components.ts b/terminus-ui/input/testing/src/test-components.ts index 61e198199..0a868dd4a 100644 --- a/terminus-ui/input/testing/src/test-components.ts +++ b/terminus-ui/input/testing/src/test-components.ts @@ -313,7 +313,7 @@ export class Clearable { @ViewChild(TsInputComponent) inputComponent: TsInputComponent; // Must be overwritten with a spy in the test - cleared = (v) => {}; + cleared = v => {}; } @Component({ diff --git a/terminus-ui/input/testing/src/test-helpers.ts b/terminus-ui/input/testing/src/test-helpers.ts index 7cd07e65d..4cfc1aeac 100644 --- a/terminus-ui/input/testing/src/test-helpers.ts +++ b/terminus-ui/input/testing/src/test-helpers.ts @@ -14,7 +14,7 @@ export function getAllInputInstances(fixture: ComponentFixture): TsInputCom if (!debugElements) { throw new Error(`'getAllInputInstances' found no inputs`); } - return debugElements.map((i) => i.componentInstance); + return debugElements.map(i => i.componentInstance); } /** diff --git a/terminus-ui/link/src/link.component.spec.ts b/terminus-ui/link/src/link.component.spec.ts index 0460ad6c0..db9dc9acb 100644 --- a/terminus-ui/link/src/link.component.spec.ts +++ b/terminus-ui/link/src/link.component.spec.ts @@ -1,6 +1,12 @@ import { APP_BASE_HREF } from '@angular/common'; -import { Component, ViewChild } from '@angular/core'; -import { ComponentFixture, tick } from '@angular/core/testing'; +import { + Component, + ViewChild, +} from '@angular/core'; +import { + ComponentFixture, + tick, +} from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { RouterModule } from '@angular/router'; @@ -37,7 +43,10 @@ describe(`TsLinkComponent`, function() { let linkComponent: TsLinkComponent; beforeEach(() => { - fixture = createComponent(TestHostComponent, [{provide: APP_BASE_HREF, useValue: '/my/app'}], [TsLinkModule, RouterModule.forRoot([])]); + fixture = createComponent(TestHostComponent, [{ + provide: APP_BASE_HREF, + useValue: '/my/app', + }], [TsLinkModule, RouterModule.forRoot([])]); component = fixture.componentInstance; linkComponent = component.linkComponent; fixture.detectChanges(); diff --git a/terminus-ui/link/src/link.component.ts b/terminus-ui/link/src/link.component.ts index 56beb34df..6d644535f 100644 --- a/terminus-ui/link/src/link.component.ts +++ b/terminus-ui/link/src/link.component.ts @@ -44,13 +44,13 @@ export class TsLinkComponent { /** * Define the icon for external links */ - public externalIcon: string = `open_in_new`; + public externalIcon = `open_in_new`; /** * Define the link's destination */ @Input() - public destination: any; + public destination: string | string[] | undefined; /** * Define if the link is to an external page @@ -62,6 +62,6 @@ export class TsLinkComponent { * Define the tabindex */ @Input() - public tabIndex: number = 0; + public tabIndex = 0; } diff --git a/terminus-ui/loading-overlay/src/loading-overlay.component.html b/terminus-ui/loading-overlay/src/loading-overlay.component.html new file mode 100644 index 000000000..9364c9d5c --- /dev/null +++ b/terminus-ui/loading-overlay/src/loading-overlay.component.html @@ -0,0 +1,19 @@ +
+ + + +
diff --git a/terminus-ui/loading-overlay/src/loading-overlay.component.ts b/terminus-ui/loading-overlay/src/loading-overlay.component.ts index c03194ac8..c7685b4d7 100644 --- a/terminus-ui/loading-overlay/src/loading-overlay.component.ts +++ b/terminus-ui/loading-overlay/src/loading-overlay.component.ts @@ -1,4 +1,5 @@ import { + ChangeDetectionStrategy, Component, ViewEncapsulation, } from '@angular/core'; @@ -19,27 +20,8 @@ import { host: { class: 'ts-loading-overlay', }, - template: ` -
- - - -
- `, + templateUrl: './loading-overlay.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, exportAs: 'tsLoadingOverlay', }) diff --git a/terminus-ui/loading-overlay/src/loading-overlay.directive.spec.ts b/terminus-ui/loading-overlay/src/loading-overlay.directive.spec.ts index 3e83d1e7d..8a9ebf8e8 100644 --- a/terminus-ui/loading-overlay/src/loading-overlay.directive.spec.ts +++ b/terminus-ui/loading-overlay/src/loading-overlay.directive.spec.ts @@ -90,7 +90,9 @@ describe(`TsLoadingOverlayDirective`, function() { directive['bodyPortalHost'] = undefined as any; directive.ngOnDestroy(); - expect(() => {directive.ngOnDestroy(); }).not.toThrow(); + expect(() => { + directive.ngOnDestroy(); + }).not.toThrow(); }); }); diff --git a/terminus-ui/loading-overlay/src/loading-overlay.directive.ts b/terminus-ui/loading-overlay/src/loading-overlay.directive.ts index be6d57cdc..2cd4df436 100644 --- a/terminus-ui/loading-overlay/src/loading-overlay.directive.ts +++ b/terminus-ui/loading-overlay/src/loading-overlay.directive.ts @@ -1,4 +1,7 @@ -import { ComponentPortal, DomPortalHost } from '@angular/cdk/portal'; +import { + ComponentPortal, + DomPortalHost, +} from '@angular/cdk/portal'; import { ApplicationRef, ComponentFactoryResolver, diff --git a/terminus-ui/login-form/src/login-form.component.html b/terminus-ui/login-form/src/login-form.component.html index 70e9da5c7..20f76afdc 100644 --- a/terminus-ui/login-form/src/login-form.component.html +++ b/terminus-ui/login-form/src/login-form.component.html @@ -3,7 +3,7 @@ [formGroup]="loginForm" fxLayout="column" *ngIf="showForm" - (keydown.enter)="loginForm.valid && submit.emit(loginForm.value)" + (keydown.enter)="loginForm?.valid && submit.emit(loginForm?.value)" >