mirror of
https://github.com/bitwarden/browser.git
synced 2025-01-02 18:17:46 +01:00
[PM-14019] Toggle Vault Filters (#11929)
* move vault headings to their own component * update aria-label to bind to the data attribute * move vault headings to the vault-v2 folder * integrate disclosure trigger to hide vault filters * remove built in margin on search component - spacing will be managed by the parent component * add event emitter so consuming components can know when disclosure status has changed * add filter badge when filters are selected and the filters are hidden * persist filter visibility state to disk * add supporting text for the filter button * remove extra file * only read from stored state on component launch. - I noticed delays when trying to use stored state as the source of truth * use two-way data binding for change event * update vault headers to use two way data binds from disclosure component - also adjust consuming changes * add border thickness * add ticket to the FIXME * move number of filters observable into service * move state coordination into filter service * only expose state and update methods from filter service * simplify observables to avoid needed state lifecycle methods * remove comment * fix test imports * update badge colors --------- Co-authored-by: Matt Bishop <mbishop@bitwarden.com>
This commit is contained in:
parent
c17f582768
commit
ca839b3d80
@ -4272,6 +4272,21 @@
|
||||
"filters": {
|
||||
"message": "Filters"
|
||||
},
|
||||
"filterVault": {
|
||||
"message": "Filter vault"
|
||||
},
|
||||
"filterApplied": {
|
||||
"message": "One filter applied"
|
||||
},
|
||||
"filterAppliedPlural": {
|
||||
"message": "$COUNT$ filters applied",
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"content": "$1",
|
||||
"example": "3"
|
||||
}
|
||||
}
|
||||
},
|
||||
"personalDetails": {
|
||||
"message": "Personal details"
|
||||
},
|
||||
|
@ -0,0 +1,38 @@
|
||||
<div class="tw-flex tw-gap-1 tw-items-center">
|
||||
<div class="tw-flex-1">
|
||||
<app-vault-v2-search></app-vault-v2-search>
|
||||
</div>
|
||||
<div class="tw-relative">
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton="bwi-sliders"
|
||||
[buttonType]="'muted'"
|
||||
[bitDisclosureTriggerFor]="disclosureRef"
|
||||
[appA11yTitle]="'filterVault' | i18n"
|
||||
aria-describedby="filters-applied"
|
||||
></button>
|
||||
<p
|
||||
class="tw-sr-only"
|
||||
id="filters-applied"
|
||||
*ngIf="buttonSupportingText$ | async as supportingText"
|
||||
>
|
||||
{{ supportingText }}
|
||||
</p>
|
||||
<div
|
||||
*ngIf="showBadge$ | async"
|
||||
class="tw-flex tw-items-center tw-justify-center tw-z-10 tw-absolute tw-rounded-full tw-h-[15px] tw-w-[15px] tw-top-[1px] tw-right-[1px] tw-text-notification-600 tw-text-[8px] tw-border-notification-600 tw-border-[0.5px] tw-border-solid tw-bg-notification-100 tw-leading-normal"
|
||||
data-testid="filter-badge"
|
||||
>
|
||||
{{ numberOfAppliedFilters$ | async }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<bit-disclosure
|
||||
#disclosureRef
|
||||
[open]="initialDisclosureVisibility$ | async"
|
||||
(openChange)="toggleFilters($event)"
|
||||
>
|
||||
<div class="tw-pt-2">
|
||||
<app-vault-list-filters></app-vault-list-filters>
|
||||
</div>
|
||||
</bit-disclosure>
|
@ -0,0 +1,162 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
import { By } from "@angular/platform-browser";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { BehaviorSubject, Subject } from "rxjs";
|
||||
|
||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { MessageSender } from "@bitwarden/common/platform/messaging";
|
||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
|
||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
|
||||
import { AutofillService } from "../../../../../autofill/services/abstractions/autofill.service";
|
||||
import { VaultPopupItemsService } from "../../../../../vault/popup/services/vault-popup-items.service";
|
||||
import {
|
||||
PopupListFilter,
|
||||
VaultPopupListFiltersService,
|
||||
} from "../../../../../vault/popup/services/vault-popup-list-filters.service";
|
||||
|
||||
import { VaultHeaderV2Component } from "./vault-header-v2.component";
|
||||
|
||||
describe("VaultHeaderV2Component", () => {
|
||||
let component: VaultHeaderV2Component;
|
||||
let fixture: ComponentFixture<VaultHeaderV2Component>;
|
||||
|
||||
const emptyForm: PopupListFilter = {
|
||||
organization: null,
|
||||
collection: null,
|
||||
folder: null,
|
||||
cipherType: null,
|
||||
};
|
||||
|
||||
const numberOfAppliedFilters$ = new BehaviorSubject<number>(0);
|
||||
const state$ = new Subject<boolean | null>();
|
||||
|
||||
// Mock state provider update
|
||||
const update = jest.fn().mockResolvedValue(undefined);
|
||||
|
||||
/** When it exists, returns the notification badge debug element */
|
||||
const getBadge = () => fixture.debugElement.query(By.css('[data-testid="filter-badge"]'));
|
||||
|
||||
beforeEach(async () => {
|
||||
update.mockClear();
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [VaultHeaderV2Component, CommonModule],
|
||||
providers: [
|
||||
{
|
||||
provide: CipherService,
|
||||
useValue: mock<CipherService>({ cipherViews$: new BehaviorSubject([]) }),
|
||||
},
|
||||
{ provide: VaultSettingsService, useValue: mock<VaultSettingsService>() },
|
||||
{ provide: FolderService, useValue: mock<FolderService>() },
|
||||
{ provide: OrganizationService, useValue: mock<OrganizationService>() },
|
||||
{ provide: CollectionService, useValue: mock<CollectionService>() },
|
||||
{ provide: PolicyService, useValue: mock<PolicyService>() },
|
||||
{ provide: SearchService, useValue: mock<SearchService>() },
|
||||
{ provide: PlatformUtilsService, useValue: mock<PlatformUtilsService>() },
|
||||
{ provide: AutofillService, useValue: mock<AutofillService>() },
|
||||
{ provide: PasswordRepromptService, useValue: mock<PasswordRepromptService>() },
|
||||
{ provide: MessageSender, useValue: mock<MessageSender>() },
|
||||
{ provide: AccountService, useValue: mock<AccountService>() },
|
||||
{ provide: LogService, useValue: mock<LogService>() },
|
||||
{
|
||||
provide: VaultPopupItemsService,
|
||||
useValue: mock<VaultPopupItemsService>({ latestSearchText$: new BehaviorSubject("") }),
|
||||
},
|
||||
{
|
||||
provide: SyncService,
|
||||
useValue: mock<SyncService>({ activeUserLastSync$: () => new Subject() }),
|
||||
},
|
||||
{ provide: ActivatedRoute, useValue: { queryParams: new BehaviorSubject({}) } },
|
||||
{ provide: I18nService, useValue: { t: (key: string) => key } },
|
||||
{
|
||||
provide: VaultPopupListFiltersService,
|
||||
useValue: {
|
||||
numberOfAppliedFilters$,
|
||||
filters$: new BehaviorSubject(emptyForm),
|
||||
filterForm: new FormBuilder().group(emptyForm),
|
||||
filterVisibilityState$: state$,
|
||||
updateFilterVisibility: update,
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: StateProvider,
|
||||
useValue: { getGlobal: () => ({ state$, update }) },
|
||||
},
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(VaultHeaderV2Component);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("does not show filter badge when no filters are selected", () => {
|
||||
state$.next(false);
|
||||
numberOfAppliedFilters$.next(0);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(getBadge()).toBeNull();
|
||||
});
|
||||
|
||||
it("does not show filter badge when disclosure is open", () => {
|
||||
state$.next(true);
|
||||
numberOfAppliedFilters$.next(1);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(getBadge()).toBeNull();
|
||||
});
|
||||
|
||||
it("shows the notification badge when there are populated filters and the disclosure is closed", async () => {
|
||||
state$.next(false);
|
||||
numberOfAppliedFilters$.next(1);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(getBadge()).not.toBeNull();
|
||||
});
|
||||
|
||||
it("displays the number of filters populated", () => {
|
||||
numberOfAppliedFilters$.next(1);
|
||||
state$.next(false);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(getBadge().nativeElement.textContent.trim()).toBe("1");
|
||||
|
||||
numberOfAppliedFilters$.next(2);
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(getBadge().nativeElement.textContent.trim()).toBe("2");
|
||||
|
||||
numberOfAppliedFilters$.next(4);
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(getBadge().nativeElement.textContent.trim()).toBe("4");
|
||||
});
|
||||
|
||||
it("defaults the initial state to true", (done) => {
|
||||
// The initial value of the `state$` variable above is undefined
|
||||
component["initialDisclosureVisibility$"].subscribe((initialVisibility) => {
|
||||
expect(initialVisibility).toBeTrue();
|
||||
done();
|
||||
});
|
||||
|
||||
// Update the state to null
|
||||
state$.next(null);
|
||||
});
|
||||
});
|
@ -0,0 +1,70 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, inject, NgZone, ViewChild } from "@angular/core";
|
||||
import { combineLatest, map, take } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { DisclosureTriggerForDirective, IconButtonModule } from "@bitwarden/components";
|
||||
|
||||
import { DisclosureComponent } from "../../../../../../../../libs/components/src/disclosure/disclosure.component";
|
||||
import { runInsideAngular } from "../../../../../platform/browser/run-inside-angular.operator";
|
||||
import { VaultPopupListFiltersService } from "../../../../../vault/popup/services/vault-popup-list-filters.service";
|
||||
import { VaultListFiltersComponent } from "../vault-list-filters/vault-list-filters.component";
|
||||
import { VaultV2SearchComponent } from "../vault-search/vault-v2-search.component";
|
||||
|
||||
@Component({
|
||||
selector: "app-vault-header-v2",
|
||||
templateUrl: "vault-header-v2.component.html",
|
||||
standalone: true,
|
||||
imports: [
|
||||
VaultV2SearchComponent,
|
||||
VaultListFiltersComponent,
|
||||
DisclosureComponent,
|
||||
IconButtonModule,
|
||||
DisclosureTriggerForDirective,
|
||||
CommonModule,
|
||||
JslibModule,
|
||||
],
|
||||
})
|
||||
export class VaultHeaderV2Component {
|
||||
@ViewChild(DisclosureComponent) disclosure: DisclosureComponent;
|
||||
|
||||
/** Emits the visibility status of the disclosure component. */
|
||||
protected isDisclosureShown$ = this.vaultPopupListFiltersService.filterVisibilityState$.pipe(
|
||||
runInsideAngular(inject(NgZone)), // Browser state updates can happen outside of `ngZone`
|
||||
map((v) => v ?? true),
|
||||
);
|
||||
|
||||
// Only use the first value to avoid an infinite loop from two-way binding
|
||||
protected initialDisclosureVisibility$ = this.isDisclosureShown$.pipe(take(1));
|
||||
|
||||
protected numberOfAppliedFilters$ = this.vaultPopupListFiltersService.numberOfAppliedFilters$;
|
||||
|
||||
/** Emits true when the number of filters badge should be applied. */
|
||||
protected showBadge$ = combineLatest([
|
||||
this.numberOfAppliedFilters$,
|
||||
this.isDisclosureShown$,
|
||||
]).pipe(map(([numberOfFilters, disclosureShown]) => numberOfFilters !== 0 && !disclosureShown));
|
||||
|
||||
protected buttonSupportingText$ = this.numberOfAppliedFilters$.pipe(
|
||||
map((numberOfFilters) => {
|
||||
if (numberOfFilters === 0) {
|
||||
return null;
|
||||
}
|
||||
if (numberOfFilters === 1) {
|
||||
return this.i18nService.t("filterApplied");
|
||||
}
|
||||
|
||||
return this.i18nService.t("filterAppliedPlural", numberOfFilters);
|
||||
}),
|
||||
);
|
||||
|
||||
constructor(
|
||||
private vaultPopupListFiltersService: VaultPopupListFiltersService,
|
||||
private i18nService: I18nService,
|
||||
) {}
|
||||
|
||||
async toggleFilters(isShown: boolean) {
|
||||
await this.vaultPopupListFiltersService.updateFilterVisibility(isShown);
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
<div role="toolbar" [ariaLabel]="'filters' | i18n">
|
||||
<div role="toolbar" [attr.aria-label]="'filters' | i18n">
|
||||
<form
|
||||
[formGroup]="filterForm"
|
||||
class="tw-gap-2 tw-mt-2 tw-grid tw-grid-cols-2 sm:tw-grid-cols-3 lg:tw-grid-cols-4"
|
||||
|
@ -1,9 +1,7 @@
|
||||
<div class="tw-mb-2">
|
||||
<bit-search
|
||||
<bit-search
|
||||
[placeholder]="'search' | i18n"
|
||||
[(ngModel)]="searchText"
|
||||
(ngModelChange)="onSearchTextChanged()"
|
||||
appAutofocus
|
||||
>
|
||||
</bit-search>
|
||||
</div>
|
||||
>
|
||||
</bit-search>
|
||||
|
@ -27,8 +27,7 @@
|
||||
slot="above-scroll-area"
|
||||
*ngIf="vaultState !== VaultStateEnum.Empty && !(loading$ | async)"
|
||||
>
|
||||
<app-vault-v2-search> </app-vault-v2-search>
|
||||
<app-vault-list-filters></app-vault-list-filters>
|
||||
<app-vault-header-v2></app-vault-header-v2>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="vaultState !== VaultStateEnum.Empty">
|
||||
|
@ -23,8 +23,7 @@ import {
|
||||
NewItemDropdownV2Component,
|
||||
NewItemInitialValues,
|
||||
} from "../vault-v2/new-item-dropdown/new-item-dropdown-v2.component";
|
||||
import { VaultListFiltersComponent } from "../vault-v2/vault-list-filters/vault-list-filters.component";
|
||||
import { VaultV2SearchComponent } from "../vault-v2/vault-search/vault-v2-search.component";
|
||||
import { VaultHeaderV2Component } from "../vault-v2/vault-header/vault-header-v2.component";
|
||||
|
||||
enum VaultState {
|
||||
Empty,
|
||||
@ -46,12 +45,11 @@ enum VaultState {
|
||||
CommonModule,
|
||||
AutofillVaultListItemsComponent,
|
||||
VaultListItemsContainerComponent,
|
||||
VaultListFiltersComponent,
|
||||
ButtonModule,
|
||||
RouterLink,
|
||||
VaultV2SearchComponent,
|
||||
NewItemDropdownV2Component,
|
||||
ScrollingModule,
|
||||
VaultHeaderV2Component,
|
||||
],
|
||||
providers: [VaultUiOnboardingService],
|
||||
})
|
||||
|
@ -9,6 +9,7 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { ProductTierType } from "@bitwarden/common/billing/enums";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
@ -50,6 +51,9 @@ describe("VaultPopupListFiltersService", () => {
|
||||
policyAppliesToActiveUser$: jest.fn(() => policyAppliesToActiveUser$),
|
||||
};
|
||||
|
||||
const state$ = new BehaviorSubject<boolean>(false);
|
||||
const update = jest.fn().mockResolvedValue(undefined);
|
||||
|
||||
beforeEach(() => {
|
||||
memberOrganizations$.next([]);
|
||||
decryptedCollections$.next([]);
|
||||
@ -83,6 +87,10 @@ describe("VaultPopupListFiltersService", () => {
|
||||
provide: PolicyService,
|
||||
useValue: policyService,
|
||||
},
|
||||
{
|
||||
provide: StateProvider,
|
||||
useValue: { getGlobal: () => ({ state$, update }) },
|
||||
},
|
||||
{ provide: FormBuilder, useClass: FormBuilder },
|
||||
],
|
||||
});
|
||||
@ -102,6 +110,20 @@ describe("VaultPopupListFiltersService", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("numberOfAppliedFilters$", () => {
|
||||
it("updates as the form value changes", (done) => {
|
||||
service.numberOfAppliedFilters$.subscribe((number) => {
|
||||
expect(number).toBe(2);
|
||||
done();
|
||||
});
|
||||
|
||||
service.filterForm.patchValue({
|
||||
organization: { id: "1234" } as Organization,
|
||||
folder: { id: "folder11" } as FolderView,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("organizations$", () => {
|
||||
it('does not add "myVault" to the list of organizations when there are no organizations', (done) => {
|
||||
memberOrganizations$.next([]);
|
||||
@ -451,4 +473,24 @@ describe("VaultPopupListFiltersService", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("filterVisibilityState", () => {
|
||||
it("exposes stored state through filterVisibilityState$", (done) => {
|
||||
state$.next(true);
|
||||
|
||||
service.filterVisibilityState$.subscribe((filterVisibility) => {
|
||||
expect(filterVisibility).toBeTrue();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("updates stored filter state", async () => {
|
||||
await service.updateFilterVisibility(false);
|
||||
|
||||
expect(update).toHaveBeenCalledOnce();
|
||||
// Get callback passed to `update`
|
||||
const updateCallback = update.mock.calls[0][0];
|
||||
expect(updateCallback()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -6,6 +6,7 @@ import {
|
||||
distinctUntilChanged,
|
||||
map,
|
||||
Observable,
|
||||
shareReplay,
|
||||
startWith,
|
||||
switchMap,
|
||||
tap,
|
||||
@ -20,6 +21,11 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga
|
||||
import { ProductTierType } from "@bitwarden/common/billing/enums";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import {
|
||||
KeyDefinition,
|
||||
StateProvider,
|
||||
VAULT_SETTINGS_DISK,
|
||||
} from "@bitwarden/common/platform/state";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
@ -29,6 +35,10 @@ import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||
import { ServiceUtils } from "@bitwarden/common/vault/service-utils";
|
||||
import { ChipSelectOption } from "@bitwarden/components";
|
||||
|
||||
const FILTER_VISIBILITY_KEY = new KeyDefinition<boolean>(VAULT_SETTINGS_DISK, "filterVisibility", {
|
||||
deserializer: (obj) => obj,
|
||||
});
|
||||
|
||||
/** All available cipher filters */
|
||||
export type PopupListFilter = {
|
||||
organization: Organization | null;
|
||||
@ -66,6 +76,15 @@ export class VaultPopupListFiltersService {
|
||||
startWith(INITIAL_FILTERS),
|
||||
) as Observable<PopupListFilter>;
|
||||
|
||||
/** Emits the number of applied filters. */
|
||||
numberOfAppliedFilters$ = this.filters$.pipe(
|
||||
map((filters) => Object.values(filters).filter((filter) => Boolean(filter)).length),
|
||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
);
|
||||
|
||||
/** Stored state for the visibility of the filters. */
|
||||
private filterVisibilityState = this.stateProvider.getGlobal(FILTER_VISIBILITY_KEY);
|
||||
|
||||
/**
|
||||
* Static list of ciphers views used in synchronous context
|
||||
*/
|
||||
@ -89,12 +108,16 @@ export class VaultPopupListFiltersService {
|
||||
private collectionService: CollectionService,
|
||||
private formBuilder: FormBuilder,
|
||||
private policyService: PolicyService,
|
||||
private stateProvider: StateProvider,
|
||||
) {
|
||||
this.filterForm.controls.organization.valueChanges
|
||||
.pipe(takeUntilDestroyed())
|
||||
.subscribe(this.validateOrganizationChange.bind(this));
|
||||
}
|
||||
|
||||
/** Stored state for the visibility of the filters. */
|
||||
filterVisibilityState$ = this.filterVisibilityState.state$;
|
||||
|
||||
/**
|
||||
* Observable whose value is a function that filters an array of `CipherView` objects based on the current filters
|
||||
*/
|
||||
@ -332,6 +355,11 @@ export class VaultPopupListFiltersService {
|
||||
),
|
||||
);
|
||||
|
||||
/** Updates the stored state for filter visibility. */
|
||||
async updateFilterVisibility(isVisible: boolean): Promise<void> {
|
||||
await this.filterVisibilityState.update(() => isVisible);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the given item into the `ChipSelectOption` structure
|
||||
*/
|
||||
|
@ -1,4 +1,11 @@
|
||||
import { Component, HostBinding, Input, booleanAttribute } from "@angular/core";
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
HostBinding,
|
||||
Input,
|
||||
Output,
|
||||
booleanAttribute,
|
||||
} from "@angular/core";
|
||||
|
||||
let nextId = 0;
|
||||
|
||||
@ -8,14 +15,26 @@ let nextId = 0;
|
||||
template: `<ng-content></ng-content>`,
|
||||
})
|
||||
export class DisclosureComponent {
|
||||
private _open: boolean;
|
||||
|
||||
/** Emits the visibility of the disclosure content */
|
||||
@Output() openChange = new EventEmitter<boolean>();
|
||||
|
||||
/**
|
||||
* Optionally init the disclosure in its opened state
|
||||
*/
|
||||
@Input({ transform: booleanAttribute }) open?: boolean = false;
|
||||
@Input({ transform: booleanAttribute }) set open(isOpen: boolean) {
|
||||
this._open = isOpen;
|
||||
this.openChange.emit(isOpen);
|
||||
}
|
||||
|
||||
@HostBinding("class") get classList() {
|
||||
return this.open ? "" : "tw-hidden";
|
||||
}
|
||||
|
||||
@HostBinding("id") id = `bit-disclosure-${nextId++}`;
|
||||
|
||||
get open(): boolean {
|
||||
return this._open;
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user