1
0
mirror of https://github.com/bitwarden/browser.git synced 2025-01-02 18:17:46 +01:00

[CL-500] Add disclosure component and directive (#11865)

This commit is contained in:
Victoria League 2024-11-07 16:54:49 -05:00 committed by GitHub
parent f206e0f817
commit e8dac0cc12
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 152 additions and 15 deletions

View File

@ -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;
}
}

View File

@ -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++}`;
}

View File

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

View File

@ -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>
`,
}),
};

View File

@ -0,0 +1,2 @@
export * from "./disclosure-trigger-for.directive";
export * from "./disclosure.component";

View File

@ -52,10 +52,14 @@ const styles: Record<IconButtonType, string[]> = {
"tw-bg-transparent", "tw-bg-transparent",
"!tw-text-muted", "!tw-text-muted",
"tw-border-transparent", "tw-border-transparent",
"aria-expanded:tw-bg-text-muted",
"aria-expanded:!tw-text-contrast",
"hover:tw-bg-transparent-hover", "hover:tw-bg-transparent-hover",
"hover:tw-border-primary-700", "hover:tw-border-primary-700",
"focus-visible:before:tw-ring-primary-700", "focus-visible:before:tw-ring-primary-700",
"disabled:tw-opacity-60", "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-border-transparent",
"disabled:hover:tw-bg-transparent", "disabled:hover:tw-bg-transparent",
...focusRing, ...focusRing,

View File

@ -29,8 +29,6 @@ Icon buttons can be found in other components such as: the
[dialog](?path=/docs/component-library-dialogs--docs), and [dialog](?path=/docs/component-library-dialogs--docs), and
[table](?path=/docs/component-library-table--docs). [table](?path=/docs/component-library-table--docs).
<Story id="component-library-banner--premium" />
## Styles ## Styles
There are 4 common styles for button main, muted, contrast, and danger. The other styles follow the There are 4 common styles for button main, muted, contrast, and danger. The other styles follow the
@ -40,48 +38,48 @@ button component styles.
Used for general icon buttons appearing on the themes main `background` Used for general icon buttons appearing on the themes main `background`
<Story id="component-library-icon-button--main" /> <Story of={stories.Main} />
### Muted ### Muted
Used for low emphasis icon buttons appearing on the themes main `background` Used for low emphasis icon buttons appearing on the themes main `background`
<Story id="component-library-icon-button--muted" /> <Story of={stories.Muted} />
### Contrast ### Contrast
Used on a themes colored or contrasting backgrounds such as in the navigation or on toasts and Used on a themes colored or contrasting backgrounds such as in the navigation or on toasts and
banners. banners.
<Story id="component-library-icon-button--contrast" /> <Story of={stories.Contrast} />
### Danger ### Danger
Danger is used for “trash” actions throughout the experience, most commonly in the bottom right of Danger is used for “trash” actions throughout the experience, most commonly in the bottom right of
the dialog component. the dialog component.
<Story id="component-library-icon-button--danger" /> <Story of={stories.Danger} />
### Primary ### Primary
Used in place of the main button component if no text is used. This allows the button to display Used in place of the main button component if no text is used. This allows the button to display
square. square.
<Story id="component-library-icon-button--primary" /> <Story of={stories.Primary} />
### Secondary ### Secondary
Used in place of the main button component if no text is used. This allows the button to display Used in place of the main button component if no text is used. This allows the button to display
square. square.
<Story id="component-library-icon-button--secondary" /> <Story of={stories.Secondary} />
### Light ### Light
Used on a background that is dark in both light theme and dark theme. Example: end user navigation Used on a background that is dark in both light theme and dark theme. Example: end user navigation
styles. 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 **Note:** Main and contrast styles appear on backgrounds where using `primary-700` as a focus
indicator does not meet WCAG graphic contrast guidelines. indicator does not meet WCAG graphic contrast guidelines.
@ -95,11 +93,11 @@ with less padding around the icon, such as in the navigation component.
### Small ### Small
<Story id="component-library-icon-button--small" /> <Story of={stories.Small} />
### Default ### Default
<Story id="component-library-icon-button--default" /> <Story of={stories.Default} />
## Accessibility ## Accessibility

View File

@ -23,7 +23,7 @@ type Story = StoryObj<BitIconButtonComponent>;
export const Default: Story = { export const Default: Story = {
render: (args) => ({ render: (args) => ({
props: args, props: args,
template: ` template: /*html*/ `
<div class="tw-space-x-4"> <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="main" [size]="size">Button</button>
<button bitIconButton="bwi-plus" [disabled]="disabled" [loading]="loading" buttonType="muted" [size]="size">Button</button> <button bitIconButton="bwi-plus" [disabled]="disabled" [loading]="loading" buttonType="muted" [size]="size">Button</button>
@ -56,7 +56,7 @@ export const Small: Story = {
export const Primary: Story = { export const Primary: Story = {
render: (args) => ({ render: (args) => ({
props: args, props: args,
template: ` template: /*html*/ `
<button bitIconButton="bwi-plus" [disabled]="disabled" [loading]="loading" [buttonType]="buttonType" [size]="size">Button</button> <button bitIconButton="bwi-plus" [disabled]="disabled" [loading]="loading" [buttonType]="buttonType" [size]="size">Button</button>
`, `,
}), }),
@ -96,7 +96,7 @@ export const Muted: Story = {
export const Light: Story = { export const Light: Story = {
render: (args) => ({ render: (args) => ({
props: args, props: args,
template: ` template: /*html*/ `
<div class="tw-bg-background-alt2 tw-p-6 tw-w-full tw-inline-block"> <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> <button bitIconButton="bwi-plus" [disabled]="disabled" [loading]="loading" [buttonType]="buttonType" [size]="size">Button</button>
</div> </div>
@ -110,7 +110,7 @@ export const Light: Story = {
export const Contrast: Story = { export const Contrast: Story = {
render: (args) => ({ render: (args) => ({
props: args, props: args,
template: ` template: /*html*/ `
<div class="tw-bg-primary-600 tw-p-6 tw-w-full tw-inline-block"> <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> <button bitIconButton="bwi-plus" [disabled]="disabled" [loading]="loading" [buttonType]="buttonType" [size]="size">Button</button>
</div> </div>

View File

@ -13,6 +13,7 @@ export * from "./chip-select";
export * from "./color-password"; export * from "./color-password";
export * from "./container"; export * from "./container";
export * from "./dialog"; export * from "./dialog";
export * from "./disclosure";
export * from "./form-field"; export * from "./form-field";
export * from "./icon-button"; export * from "./icon-button";
export * from "./icon"; export * from "./icon";