1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-12 06:13:38 +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( await this.vaultPopupSectionService.updateSectionOpenStoredState(
this.collapsibleKey()!, 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"; 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({ @Directive({
selector: "[bitDisclosureTriggerFor]", selector: "[bitDisclosureTriggerFor]",
exportAs: "disclosureTriggerFor", exportAs: "disclosureTriggerFor",
host: {
"[attr.aria-expanded]": "ariaExpanded()",
"[attr.aria-controls]": "ariaControls()",
"(click)": "toggle()",
},
}) })
export class DisclosureTriggerForDirective { export class DisclosureTriggerForDirective {
/** /**
@@ -12,15 +22,11 @@ export class DisclosureTriggerForDirective {
*/ */
readonly disclosure = input.required<DisclosureComponent>({ alias: "bitDisclosureTriggerFor" }); readonly disclosure = input.required<DisclosureComponent>({ alias: "bitDisclosureTriggerFor" });
@HostBinding("attr.aria-expanded") get ariaExpanded() { protected readonly ariaExpanded = computed(() => this.disclosure().open());
return this.disclosure().open;
}
@HostBinding("attr.aria-controls") get ariaControls() { protected readonly ariaControls = computed(() => this.disclosure().id);
return this.disclosure().id;
}
@HostListener("click") click() { protected toggle() {
this.disclosure().open = !this.disclosure().open; this.disclosure().open.update((open) => !open);
} }
} }

View File

@@ -1,70 +1,50 @@
import { import { ChangeDetectionStrategy, Component, computed, model } from "@angular/core";
Component,
EventEmitter,
HostBinding,
Input,
Output,
booleanAttribute,
} from "@angular/core";
let nextId = 0; 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. * 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: * To compose a disclosure and trigger:
*
* 1. Create a trigger component (see "Supported Trigger Components" section below) * 1. Create a trigger component (see "Supported Trigger Components" section below)
* 2. Create a `bit-disclosure` * 2. Create a `bit-disclosure`
* 3. Set a template reference on the `bit-disclosure` * 3. Set a template reference on the `bit-disclosure`
* 4. Use the `bitDisclosureTriggerFor` directive on the trigger component, and pass it the `bit-disclosure` template reference * 4. Use the `bitDisclosureTriggerFor` directive on the trigger component, and pass it the `bit-disclosure` template reference
* 5. Set the `open` property on the `bit-disclosure` to init the disclosure as either currently expanded or currently collapsed. The disclosure will default to `false`, meaning it defaults to being hidden. * 5. Set the `open` property on the `bit-disclosure` to init the disclosure as either currently expanded or currently collapsed. The disclosure will default to `false`, meaning it defaults to being hidden.
* *
* @example * @example
* *
* ```html * ```html
* <button * <button
* type="button" * type="button"
* bitIconButton="bwi-sliders" * bitIconButton="bwi-sliders"
* [buttonType]="'muted'" * buttonType="muted"
* [bitDisclosureTriggerFor]="disclosureRef" * [bitDisclosureTriggerFor]="disclosureRef"
* [label]="'Settings' | i18n" * [label]="'Settings' | i18n"
* ></button> * ></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({ @Component({
selector: "bit-disclosure", selector: "bit-disclosure",
template: `<ng-content></ng-content>`, template: `<ng-content></ng-content>`,
changeDetection: ChangeDetectionStrategy.OnPush,
host: {
"[class]": "classList()",
"[id]": "id",
},
}) })
export class DisclosureComponent { 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: readonly open = model<boolean>(false);
// 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;
}
@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 /> <Title />
<Description /> <Description />
<Canvas of={stories.DisclosureWithIconButton} /> <Canvas of={stories.DisclosureOpen} />
## Supported Trigger Components ## Supported Trigger Components

View File

@@ -36,13 +36,30 @@ export default {
type Story = StoryObj<DisclosureComponent>; type Story = StoryObj<DisclosureComponent>;
export const DisclosureWithIconButton: Story = { export const DisclosureOpen: Story = {
args: {
open: true,
},
render: (args) => ({ render: (args) => ({
props: args, props: args,
template: /*html*/ ` 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> </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>
`, `,
}), }),
}; };