1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-12 14:23:32 +00:00

[CL-738] Migrate disclosure component (#17206)

This commit is contained in:
Oscar Hinton
2025-11-13 23:02:38 +01:00
committed by GitHub
parent 7ba3924a4f
commit d95d86d05e
5 changed files with 72 additions and 69 deletions

View File

@@ -428,7 +428,7 @@ export class VaultListItemsContainerComponent implements AfterViewInit {
await this.vaultPopupSectionService.updateSectionOpenStoredState(
this.collapsibleKey()!,
this.disclosure.open,
this.disclosure.open(),
);
}

View File

@@ -1,10 +1,20 @@
import { Directive, HostBinding, HostListener, input } from "@angular/core";
import { Directive, computed, input } from "@angular/core";
import { DisclosureComponent } from "./disclosure.component";
/**
* Directive that connects a trigger element (like a button) to a disclosure component.
* Automatically handles click events to toggle the disclosure open/closed state and
* manages ARIA attributes for accessibility.
*/
@Directive({
selector: "[bitDisclosureTriggerFor]",
exportAs: "disclosureTriggerFor",
host: {
"[attr.aria-expanded]": "ariaExpanded()",
"[attr.aria-controls]": "ariaControls()",
"(click)": "toggle()",
},
})
export class DisclosureTriggerForDirective {
/**
@@ -12,15 +22,11 @@ export class DisclosureTriggerForDirective {
*/
readonly disclosure = input.required<DisclosureComponent>({ alias: "bitDisclosureTriggerFor" });
@HostBinding("attr.aria-expanded") get ariaExpanded() {
return this.disclosure().open;
}
protected readonly ariaExpanded = computed(() => this.disclosure().open());
@HostBinding("attr.aria-controls") get ariaControls() {
return this.disclosure().id;
}
protected readonly ariaControls = computed(() => this.disclosure().id);
@HostListener("click") click() {
this.disclosure().open = !this.disclosure().open;
protected toggle() {
this.disclosure().open.update((open) => !open);
}
}

View File

@@ -1,19 +1,12 @@
import {
Component,
EventEmitter,
HostBinding,
Input,
Output,
booleanAttribute,
} from "@angular/core";
import { ChangeDetectionStrategy, Component, computed, model } from "@angular/core";
let nextId = 0;
/**
* The `bit-disclosure` component is used in tandem with the `bitDisclosureTriggerFor` directive to create an accessible content area whose visibility is controlled by a trigger button.
*
* To compose a disclosure and trigger:
*
* 1. Create a trigger component (see "Supported Trigger Components" section below)
* 2. Create a `bit-disclosure`
* 3. Set a template reference on the `bit-disclosure`
@@ -26,45 +19,32 @@ let nextId = 0;
* <button
* type="button"
* bitIconButton="bwi-sliders"
* [buttonType]="'muted'"
* buttonType="muted"
* [bitDisclosureTriggerFor]="disclosureRef"
* [label]="'Settings' | i18n"
* ></button>
* <bit-disclosure #disclosureRef open>click button to hide this content</bit-disclosure>
* <bit-disclosure #disclosureRef [(open)]="isOpen">click button to hide this content</bit-disclosure>
* ```
*
*/
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
@Component({
selector: "bit-disclosure",
template: `<ng-content></ng-content>`,
changeDetection: ChangeDetectionStrategy.OnPush,
host: {
"[class]": "classList()",
"[id]": "id",
},
})
export class DisclosureComponent {
/** Emits the visibility of the disclosure content */
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
// eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref
@Output() openChange = new EventEmitter<boolean>();
private _open?: boolean;
/**
* Optionally init the disclosure in its opened state
* Controls the visibility of the disclosure content.
*/
// TODO: Skipped for signal migration because:
// Accessor inputs cannot be migrated as they are too complex.
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
// eslint-disable-next-line @angular-eslint/prefer-signals
@Input({ transform: booleanAttribute }) set open(isOpen: boolean) {
this._open = isOpen;
this.openChange.emit(isOpen);
}
get open(): boolean {
return !!this._open;
}
readonly open = model<boolean>(false);
@HostBinding("class") get classList() {
return this.open ? "" : "tw-hidden";
}
/**
* Autogenerated id.
*/
readonly id = `bit-disclosure-${nextId++}`;
@HostBinding("id") id = `bit-disclosure-${nextId++}`;
protected readonly classList = computed(() => (this.open() ? "" : "tw-hidden"));
}

View File

@@ -11,7 +11,7 @@ import { DisclosureComponent, DisclosureTriggerForDirective } from "@bitwarden/c
<Title />
<Description />
<Canvas of={stories.DisclosureWithIconButton} />
<Canvas of={stories.DisclosureOpen} />
## Supported Trigger Components

View File

@@ -36,13 +36,30 @@ export default {
type Story = StoryObj<DisclosureComponent>;
export const DisclosureWithIconButton: Story = {
export const DisclosureOpen: Story = {
args: {
open: true,
},
render: (args) => ({
props: args,
template: /*html*/ `
<button type="button" label="Settings" bitIconButton="bwi-sliders" [buttonType]="'muted'" [bitDisclosureTriggerFor]="disclosureRef">
<button type="button" label="Settings" bitIconButton="bwi-sliders" buttonType="muted" [bitDisclosureTriggerFor]="disclosureRef">
</button>
<bit-disclosure #disclosureRef class="tw-text-main tw-block" open>click button to hide this content</bit-disclosure>
<bit-disclosure #disclosureRef class="tw-text-main tw-block" [(open)]="open">click button to hide this content</bit-disclosure>
`,
}),
};
export const DisclosureClosed: Story = {
args: {
open: false,
},
render: (args) => ({
props: args,
template: /*html*/ `
<button type="button" label="Settings" bitIconButton="bwi-sliders" buttonType="muted" [bitDisclosureTriggerFor]="disclosureRef">
</button>
<bit-disclosure #disclosureRef class="tw-text-main tw-block" [(open)]="open">click button to hide this content</bit-disclosure>
`,
}),
};