From d6a6833280085434acdb04f42e2b1fccc8c67bc6 Mon Sep 17 00:00:00 2001 From: Dominik Messner Date: Wed, 11 Dec 2019 14:37:21 +0100 Subject: [PATCH] feat(filter-field): Add input to disable the whole filter field --- .../filter-field-demo.component.html | 4 + .../filter-field-demo.component.ts | 9 +- components/filter-field/README.md | 10 +- .../src/filter-field-tag/filter-field-tag.ts | 1 + components/filter-field/src/filter-field.html | 1 + components/filter-field/src/filter-field.scss | 11 +- .../filter-field/src/filter-field.spec.ts | 116 ++++++++++++++++++ components/filter-field/src/filter-field.ts | 65 ++++++++-- .../filter-field-disabled-example.html | 6 + .../filter-field-disabled-example.ts | 78 ++++++++++++ .../filter-field-examples.module.ts | 2 + 11 files changed, 289 insertions(+), 14 deletions(-) create mode 100644 libs/examples/src/filter-field/filter-field-disabled-example/filter-field-disabled-example.html create mode 100644 libs/examples/src/filter-field/filter-field-disabled-example/filter-field-disabled-example.ts diff --git a/apps/dev/src/filter-field/filter-field-demo.component.html b/apps/dev/src/filter-field/filter-field-demo.component.html index 0e6efba399..5b49b9d26b 100644 --- a/apps/dev/src/filter-field/filter-field-demo.component.html +++ b/apps/dev/src/filter-field/filter-field-demo.component.html @@ -1,6 +1,7 @@ Toggle loading + diff --git a/apps/dev/src/filter-field/filter-field-demo.component.ts b/apps/dev/src/filter-field/filter-field-demo.component.ts index 1d1a7ac088..0e8c24b4ca 100644 --- a/apps/dev/src/filter-field/filter-field-demo.component.ts +++ b/apps/dev/src/filter-field/filter-field-demo.component.ts @@ -129,10 +129,12 @@ export class FilterFieldDemo implements AfterViewInit, OnDestroy { private _activeDataSourceName = 'TEST_DATA'; private _tagChangesSub = Subscription.EMPTY; + private _timerHandle: number; _firstTag: DtFilterFieldTag; _dataSource = new DtFilterFieldDefaultDataSource(TEST_DATA); _loading = false; + _disabled = false; ngAfterViewInit(): void { this.filterField.currentTags.subscribe(tags => { @@ -153,14 +155,17 @@ export class FilterFieldDemo implements AfterViewInit, OnDestroy { currentFilterChanges( event: DtFilterFieldCurrentFilterChangeEvent, ): void { + // Cancel current timer if running + clearTimeout(this._timerHandle); + if (event.currentFilter[0] === TEST_DATA.autocomplete[2]) { // Simulate async data loading - setTimeout(() => { + this._timerHandle = setTimeout(() => { this._dataSource.data = TEST_DATA_ASYNC; }, 2000); } else if (event.currentFilter[0] === TEST_DATA.autocomplete[3]) { // Simulate async data loading - setTimeout(() => { + this._timerHandle = setTimeout(() => { this._dataSource.data = TEST_DATA_ASYNC_2; }, 2000); } diff --git a/components/filter-field/README.md b/components/filter-field/README.md index 9e68f33e3e..f16168aa81 100644 --- a/components/filter-field/README.md +++ b/components/filter-field/README.md @@ -24,6 +24,7 @@ class MyModule {} | `filters` | `any[][]` | | The currently selected filters. This input can also be used to programmatically add filters to the filter-field. | | `label` | `string` | | The label for the input field. Can be set to something like "Filter by". | | `loading` | `boolean` | `false` | Whether the filter-field is loading data and should show a loading spinner. | +| `disabled` | `boolean` | `false` | Whether the filter-field is disabled. | | `aria-label` | `string` | | Sets the value for the Aria-Label attribute. | ## Outputs @@ -178,9 +179,16 @@ in the [click dummy](https://invis.io/PCG28RGDUFE). +### Disabled state + +By setting the `disabled`-property to true, the whole filter field including all +tags get disabled and therefore cannot be modified by the user. + + + ### Readonly, non-deletable & non-editable tags -The filter filed creates a `DtFilterFieldTag` for each active filter. You can +The filter field creates a `DtFilterFieldTag` for each active filter. You can get subscribe to the list of current tags with the `currentTags` observable. By using the utility method `getTagForFilter` you can find a `DtFilterFieldTag` instance created for a given filter. After getting the tag instance for your diff --git a/components/filter-field/src/filter-field-tag/filter-field-tag.ts b/components/filter-field/src/filter-field-tag/filter-field-tag.ts index 04913facfd..ccfaa7a80a 100644 --- a/components/filter-field/src/filter-field-tag/filter-field-tag.ts +++ b/components/filter-field/src/filter-field-tag/filter-field-tag.ts @@ -78,6 +78,7 @@ export class DtFilterFieldTag implements OnDestroy { /** Whether the tag is disabled. */ // Note: The disabled mixin can not be used here because the CD needs to be triggerd after it has been set // to reflect the state when programatically setting the property. + @Input() get disabled(): boolean { return !this.editable && !this.deletable; } diff --git a/components/filter-field/src/filter-field.html b/components/filter-field/src/filter-field.html index c33cd4b5da..31ec000417 100644 --- a/components/filter-field/src/filter-field.html +++ b/components/filter-field/src/filter-field.html @@ -37,6 +37,7 @@ [dtFilterFieldRangeDisabled]="!(_currentDef && !!_currentDef!.range) || loading" (keydown)="_handleInputKeyDown($event)" [value]="_inputValue" + [disabled]="disabled" /> { expect(document.activeElement).toBe(input); }); + describe('disabled', () => { + it('should disable the input if filter field is disabled', () => { + // when + filterField.disabled = true; + fixture.detectChanges(); + + // then + const input = fixture.debugElement.query( + By.css('.dt-filter-field-disabled'), + ).nativeElement; + expect(input).toBeTruthy(); + }); + + it('should disable all tags if filter field is disabled', fakeAsync(() => { + // given + fixture.componentInstance.dataSource.data = TEST_DATA_SINGLE_DISTINCT; + fixture.detectChanges(); + + filterField.focus(); + zone.simulateMicrotasksEmpty(); + zone.simulateZoneExit(); + fixture.detectChanges(); + + let options = getOptions(overlayContainerElement); + + const autOption = options[0]; + autOption.click(); + zone.simulateMicrotasksEmpty(); + fixture.detectChanges(); + zone.simulateZoneExit(); + + options = getOptions(overlayContainerElement); + const viennaOption = options[0]; + viennaOption.click(); + zone.simulateMicrotasksEmpty(); + fixture.detectChanges(); + + // when + filterField.disabled = true; + fixture.detectChanges(); + + // then + const subscription = filterField.currentTags.subscribe(tags => { + for (const dtFilterFieldTag of tags) { + expect(dtFilterFieldTag.disabled).toBeTruthy(); + } + }); + tick(); + subscription.unsubscribe(); + })); + + it('should restore the previous state of tags if filter field gets enabled', fakeAsync(() => { + // given + fixture.componentInstance.dataSource.data = TEST_DATA; + fixture.detectChanges(); + + // Add filter "AUT - Vienna" + filterField.focus(); + zone.simulateMicrotasksEmpty(); + zone.simulateZoneExit(); + fixture.detectChanges(); + + let options = getOptions(overlayContainerElement); + const autOption = options[0]; + autOption.click(); + zone.simulateMicrotasksEmpty(); + fixture.detectChanges(); + zone.simulateZoneExit(); + + options = getOptions(overlayContainerElement); + const viennaOption = options[1]; + viennaOption.click(); + zone.simulateMicrotasksEmpty(); + fixture.detectChanges(); + + // Add filter "USA - Los Angeles" + filterField.focus(); + zone.simulateMicrotasksEmpty(); + zone.simulateZoneExit(); + fixture.detectChanges(); + + options = getOptions(overlayContainerElement); + const usOption = options[1]; + usOption.click(); + zone.simulateMicrotasksEmpty(); + fixture.detectChanges(); + zone.simulateZoneExit(); + + options = getOptions(overlayContainerElement); + const losAngelesOption = options[0]; + losAngelesOption.click(); + zone.simulateMicrotasksEmpty(); + fixture.detectChanges(); + + // Disable filter "AUT - Vienna" + const sub1 = filterField.currentTags.subscribe( + tags => (tags[0].disabled = true), + ); + tick(); + sub1.unsubscribe(); + + // when + filterField.disabled = true; + filterField.disabled = false; + fixture.detectChanges(); + + // then + const sub2 = filterField.currentTags.subscribe(tags => { + expect(tags[0].disabled).toBeTruthy(); + expect(tags[1].disabled).toBeFalsy(); + }); + tick(); + sub2.unsubscribe(); + })); + }); + describe('labeling', () => { it('should create an label with an filter icon', () => { const label = fixture.debugElement.query( diff --git a/components/filter-field/src/filter-field.ts b/components/filter-field/src/filter-field.ts index ea6a9dcdf4..024426b106 100644 --- a/components/filter-field/src/filter-field.ts +++ b/components/filter-field/src/filter-field.ts @@ -52,13 +52,13 @@ import { ViewEncapsulation, } from '@angular/core'; import { + fromEvent, + merge, Observable, + of as observableOf, ReplaySubject, Subject, Subscription, - fromEvent, - merge, - of as observableOf, } from 'rxjs'; import { debounceTime, @@ -79,6 +79,7 @@ import { DtAutocompleteTrigger, } from '@dynatrace/barista-components/autocomplete'; import { + CanDisable, DT_ERROR_ENTER_ANIMATION, DT_ERROR_ENTER_DELAYED_ANIMATION, ErrorStateMatcher, @@ -159,8 +160,10 @@ export const DT_FILTER_FIELD_TYPING_DEBOUNCE = 200; styleUrls: ['filter-field.scss'], host: { class: 'dt-filter-field', + '[class.dt-filter-field-disabled]': 'disabled', '(click)': '_handleHostClick($event)', }, + inputs: ['disabled'], encapsulation: ViewEncapsulation.Emulated, preserveWhitespaces: false, changeDetection: ChangeDetectionStrategy.OnPush, @@ -174,7 +177,8 @@ export const DT_FILTER_FIELD_TYPING_DEBOUNCE = 200; ]), ], }) -export class DtFilterField implements AfterViewInit, OnDestroy, OnChanges { +export class DtFilterField + implements CanDisable, AfterViewInit, OnDestroy, OnChanges { /** Label for the filter field (e.g. "Filter by"). Will be placed next to the filter icon. */ @Input() label = ''; @@ -234,6 +238,45 @@ export class DtFilterField implements AfterViewInit, OnDestroy, OnChanges { /** Set the Aria-Label attribute */ @Input('aria-label') ariaLabel = ''; + /** Whether the filter field is disabled. */ + @Input() + get disabled(): boolean { + return this._disabled; + } + set disabled(value: boolean) { + // tslint:disable: deprecation + const coerced = coerceBooleanProperty(value); + if (coerced !== this._disabled) { + this._disabled = coerced; + + if (!this.tags || this.tags.length === 0) { + return; + } + + if (this._disabled) { + this.closeFilterPanels(); + + this.tags.forEach(item => { + this._previousTagDisabledState.set(item, item.disabled); + item.disabled = this._disabled; + }); + } else { + this.tags.forEach( + item => (item.disabled = !!this._previousTagDisabledState.get(item)), + ); + } + + this._changeDetectorRef.markForCheck(); + } + + // tslint:enable: deprecation + } + private _disabled = false; + private _previousTagDisabledState: Map = new Map< + DtFilterFieldTag, + boolean + >(); + /** Emits an event with the current value of the input field every time the user types. */ @Output() readonly inputChange = new EventEmitter(); @@ -554,11 +597,7 @@ export class DtFilterField implements AfterViewInit, OnDestroy, OnChanges { ); } } else if (keyCode === ESCAPE || (keyCode === UP_ARROW && event.altKey)) { - this._autocompleteTrigger.closePanel(); - this._filterfieldRangeTrigger.closePanel(); - if (this._editModeStashedValue) { - this._cancelEditMode(); - } + this.closeFilterPanels(); } else { if (this._inputFieldKeyboardLocked) { return; @@ -1173,5 +1212,13 @@ export class DtFilterField implements AfterViewInit, OnDestroy, OnChanges { }); this._switchToRootDef(false); } + + private closeFilterPanels(): void { + this._autocompleteTrigger.closePanel(); + this._filterfieldRangeTrigger.closePanel(); + if (this._editModeStashedValue) { + this._cancelEditMode(); + } + } } // tslint:disable:max-file-line-count diff --git a/libs/examples/src/filter-field/filter-field-disabled-example/filter-field-disabled-example.html b/libs/examples/src/filter-field/filter-field-disabled-example/filter-field-disabled-example.html new file mode 100644 index 0000000000..e441e665ab --- /dev/null +++ b/libs/examples/src/filter-field/filter-field-disabled-example/filter-field-disabled-example.html @@ -0,0 +1,6 @@ + diff --git a/libs/examples/src/filter-field/filter-field-disabled-example/filter-field-disabled-example.ts b/libs/examples/src/filter-field/filter-field-disabled-example/filter-field-disabled-example.ts new file mode 100644 index 0000000000..a756adfb24 --- /dev/null +++ b/libs/examples/src/filter-field/filter-field-disabled-example/filter-field-disabled-example.ts @@ -0,0 +1,78 @@ +/** + * @license + * Copyright 2020 Dynatrace LLC + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Component } from '@angular/core'; + +import { + DtFilterFieldDefaultDataSource, + DtFilterFieldDefaultDataSourceType, +} from '@dynatrace/barista-components/filter-field'; + +@Component({ + selector: 'dt-example-filter-field-disabled', + templateUrl: 'filter-field-disabled-example.html', +}) +export class DtExampleFilterFieldDisabled { + private DATA = { + autocomplete: [ + { + name: 'AUT', + autocomplete: [{ name: 'Linz' }, { name: 'Vienna' }, { name: 'Graz' }], + }, + { + name: 'USA', + autocomplete: [ + { name: 'San Francisco' }, + { name: 'Los Angeles' }, + { name: 'New York' }, + { + name: 'Custom', + suggestions: [], + validators: [], + }, + ], + }, + { + name: 'Requests per minute', + range: { + operators: { + range: true, + equal: true, + greaterThanEqual: true, + lessThanEqual: true, + }, + unit: 's', + }, + }, + ], + }; + + _dataSource = new DtFilterFieldDefaultDataSource< + DtFilterFieldDefaultDataSourceType + >(this.DATA); + + _filters = [ + // Filter AUT -> Vienna + [this.DATA.autocomplete[0], this.DATA.autocomplete[0].autocomplete![1]], + + // Filter USA -> Custom -> Miami + [ + this.DATA.autocomplete[1], + this.DATA.autocomplete[1].autocomplete![3], + 'Miami', + ], + ]; +} diff --git a/libs/examples/src/filter-field/filter-field-examples.module.ts b/libs/examples/src/filter-field/filter-field-examples.module.ts index 07cb29b983..0b3f7578e5 100644 --- a/libs/examples/src/filter-field/filter-field-examples.module.ts +++ b/libs/examples/src/filter-field/filter-field-examples.module.ts @@ -24,6 +24,7 @@ import { DtExampleFilterFieldDistinct } from './filter-field-distinct-example/fi import { DtExampleFilterFieldProgrammaticFilters } from './filter-field-programmatic-filters-example/filter-field-programmatic-filters-example'; import { DtExampleFilterFieldReadOnlyTags } from './filter-field-readonly-non-editable-tags-example/filter-field-readonly-non-editable-tags-example'; import { DtExampleFilterFieldUnique } from './filter-field-unique-example/filter-field-unique-example'; +import { DtExampleFilterFieldDisabled } from './filter-field-disabled-example/filter-field-disabled-example'; export const DT_FILTER_FIELD_EXAMPLES = [ DtExampleFilterFieldAsync, @@ -31,6 +32,7 @@ export const DT_FILTER_FIELD_EXAMPLES = [ DtExampleFilterFieldDefault, DtExampleFilterFieldDistinct, DtExampleFilterFieldProgrammaticFilters, + DtExampleFilterFieldDisabled, DtExampleFilterFieldReadOnlyTags, DtExampleFilterFieldUnique, ];