Skip to content

Commit

Permalink
[CL-500] Add disclosure component and directive (#11865)
Browse files Browse the repository at this point in the history
  • Loading branch information
vleague2 authored Nov 7, 2024
1 parent f206e0f commit e8dac0c
Show file tree
Hide file tree
Showing 9 changed files with 152 additions and 15 deletions.
27 changes: 27 additions & 0 deletions libs/components/src/disclosure/disclosure-trigger-for.directive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Directive, HostBinding, HostListener, Input } from "@angular/core";

import { DisclosureComponent } from "./disclosure.component";

@Directive({
selector: "[bitDisclosureTriggerFor]",
exportAs: "disclosureTriggerFor",
standalone: true,
})
export class DisclosureTriggerForDirective {
/**
* Accepts template reference for a bit-disclosure component instance
*/
@Input("bitDisclosureTriggerFor") disclosure: DisclosureComponent;

@HostBinding("attr.aria-expanded") get ariaExpanded() {
return this.disclosure.open;
}

@HostBinding("attr.aria-controls") get ariaControls() {
return this.disclosure.id;
}

@HostListener("click") click() {
this.disclosure.open = !this.disclosure.open;
}
}
21 changes: 21 additions & 0 deletions libs/components/src/disclosure/disclosure.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Component, HostBinding, Input, booleanAttribute } from "@angular/core";

let nextId = 0;

@Component({
selector: "bit-disclosure",
standalone: true,
template: `<ng-content></ng-content>`,
})
export class DisclosureComponent {
/**
* Optionally init the disclosure in its opened state
*/
@Input({ transform: booleanAttribute }) open?: boolean = false;

@HostBinding("class") get classList() {
return this.open ? "" : "tw-hidden";
}

@HostBinding("id") id = `bit-disclosure-${nextId++}`;
}
55 changes: 55 additions & 0 deletions libs/components/src/disclosure/disclosure.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { Meta, Story, Primary, Controls } from "@storybook/addon-docs";

import * as stories from "./disclosure.stories";

<Meta of={stories} />

```ts
import { DisclosureComponent, DisclosureTriggerForDirective } from "@bitwarden/components";
```

# Disclosure

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`
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.

```
<button
type="button"
bitIconButton="bwi-sliders"
[buttonType]="'muted'"
[bitDisclosureTriggerFor]="disclosureRef"
></button>
<bit-disclosure #disclosureRef open>click button to hide this content</bit-disclosure>
```

<Story of={stories.DisclosureWithIconButton} />

<br />
<br />

## Supported Trigger Components

This is the list of currently supported trigger components:

- Icon button `muted` variant

## Accessibility

The disclosure and trigger directive functionality follow the
[Disclosure (Show/Hide)](https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/) pattern for
accessibility, automatically handling the `aria-controls` and `aria-expanded` properties. A `button`
element must be used as the trigger for the disclosure. The `button` element must also have an
accessible label/title -- please follow the accessibility guidelines for whatever trigger component
you choose.
29 changes: 29 additions & 0 deletions libs/components/src/disclosure/disclosure.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Meta, moduleMetadata, StoryObj } from "@storybook/angular";

import { IconButtonModule } from "../icon-button";

import { DisclosureTriggerForDirective } from "./disclosure-trigger-for.directive";
import { DisclosureComponent } from "./disclosure.component";

export default {
title: "Component Library/Disclosure",
component: DisclosureComponent,
decorators: [
moduleMetadata({
imports: [DisclosureTriggerForDirective, DisclosureComponent, IconButtonModule],
}),
],
} as Meta<DisclosureComponent>;

type Story = StoryObj<DisclosureComponent>;

export const DisclosureWithIconButton: Story = {
render: (args) => ({
props: args,
template: /*html*/ `
<button type="button" 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>
`,
}),
};
2 changes: 2 additions & 0 deletions libs/components/src/disclosure/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./disclosure-trigger-for.directive";
export * from "./disclosure.component";
4 changes: 4 additions & 0 deletions libs/components/src/icon-button/icon-button.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,14 @@ const styles: Record<IconButtonType, string[]> = {
"tw-bg-transparent",
"!tw-text-muted",
"tw-border-transparent",
"aria-expanded:tw-bg-text-muted",
"aria-expanded:!tw-text-contrast",
"hover:tw-bg-transparent-hover",
"hover:tw-border-primary-700",
"focus-visible:before:tw-ring-primary-700",
"disabled:tw-opacity-60",
"aria-expanded:hover:tw-bg-secondary-700",
"aria-expanded:hover:tw-border-secondary-700",
"disabled:hover:tw-border-transparent",
"disabled:hover:tw-bg-transparent",
...focusRing,
Expand Down
20 changes: 9 additions & 11 deletions libs/components/src/icon-button/icon-button.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,6 @@ Icon buttons can be found in other components such as: the
[dialog](?path=/docs/component-library-dialogs--docs), and
[table](?path=/docs/component-library-table--docs).

<Story id="component-library-banner--premium" />

## Styles

There are 4 common styles for button main, muted, contrast, and danger. The other styles follow the
Expand All @@ -40,48 +38,48 @@ button component styles.

Used for general icon buttons appearing on the theme’s main `background`

<Story id="component-library-icon-button--main" />
<Story of={stories.Main} />

### Muted

Used for low emphasis icon buttons appearing on the theme’s main `background`

<Story id="component-library-icon-button--muted" />
<Story of={stories.Muted} />

### Contrast

Used on a theme’s colored or contrasting backgrounds such as in the navigation or on toasts and
banners.

<Story id="component-library-icon-button--contrast" />
<Story of={stories.Contrast} />

### Danger

Danger is used for “trash” actions throughout the experience, most commonly in the bottom right of
the dialog component.

<Story id="component-library-icon-button--danger" />
<Story of={stories.Danger} />

### Primary

Used in place of the main button component if no text is used. This allows the button to display
square.

<Story id="component-library-icon-button--primary" />
<Story of={stories.Primary} />

### Secondary

Used in place of the main button component if no text is used. This allows the button to display
square.

<Story id="component-library-icon-button--secondary" />
<Story of={stories.Secondary} />

### Light

Used on a background that is dark in both light theme and dark theme. Example: end user navigation
styles.

<Story id="component-library-icon-button--light" />
<Story of={stories.Light} />

**Note:** Main and contrast styles appear on backgrounds where using `primary-700` as a focus
indicator does not meet WCAG graphic contrast guidelines.
Expand All @@ -95,11 +93,11 @@ with less padding around the icon, such as in the navigation component.

### Small

<Story id="component-library-icon-button--small" />
<Story of={stories.Small} />

### Default

<Story id="component-library-icon-button--default" />
<Story of={stories.Default} />

## Accessibility

Expand Down
8 changes: 4 additions & 4 deletions libs/components/src/icon-button/icon-button.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ type Story = StoryObj<BitIconButtonComponent>;
export const Default: Story = {
render: (args) => ({
props: args,
template: `
template: /*html*/ `
<div class="tw-space-x-4">
<button bitIconButton="bwi-plus" [disabled]="disabled" [loading]="loading" buttonType="main" [size]="size">Button</button>
<button bitIconButton="bwi-plus" [disabled]="disabled" [loading]="loading" buttonType="muted" [size]="size">Button</button>
Expand Down Expand Up @@ -56,7 +56,7 @@ export const Small: Story = {
export const Primary: Story = {
render: (args) => ({
props: args,
template: `
template: /*html*/ `
<button bitIconButton="bwi-plus" [disabled]="disabled" [loading]="loading" [buttonType]="buttonType" [size]="size">Button</button>
`,
}),
Expand Down Expand Up @@ -96,7 +96,7 @@ export const Muted: Story = {
export const Light: Story = {
render: (args) => ({
props: args,
template: `
template: /*html*/ `
<div class="tw-bg-background-alt2 tw-p-6 tw-w-full tw-inline-block">
<button bitIconButton="bwi-plus" [disabled]="disabled" [loading]="loading" [buttonType]="buttonType" [size]="size">Button</button>
</div>
Expand All @@ -110,7 +110,7 @@ export const Light: Story = {
export const Contrast: Story = {
render: (args) => ({
props: args,
template: `
template: /*html*/ `
<div class="tw-bg-primary-600 tw-p-6 tw-w-full tw-inline-block">
<button bitIconButton="bwi-plus" [disabled]="disabled" [loading]="loading" [buttonType]="buttonType" [size]="size">Button</button>
</div>
Expand Down
1 change: 1 addition & 0 deletions libs/components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export * from "./chip-select";
export * from "./color-password";
export * from "./container";
export * from "./dialog";
export * from "./disclosure";
export * from "./form-field";
export * from "./icon-button";
export * from "./icon";
Expand Down

0 comments on commit e8dac0c

Please sign in to comment.