From e8dac0cc122d35d6a279fd64adda10b2df97e402 Mon Sep 17 00:00:00 2001 From: Victoria League Date: Thu, 7 Nov 2024 16:54:49 -0500 Subject: [PATCH] [CL-500] Add disclosure component and directive (#11865) --- .../disclosure-trigger-for.directive.ts | 27 +++++++++ .../src/disclosure/disclosure.component.ts | 21 +++++++ libs/components/src/disclosure/disclosure.mdx | 55 +++++++++++++++++++ .../src/disclosure/disclosure.stories.ts | 29 ++++++++++ libs/components/src/disclosure/index.ts | 2 + .../src/icon-button/icon-button.component.ts | 4 ++ .../src/icon-button/icon-button.mdx | 20 +++---- .../src/icon-button/icon-button.stories.ts | 8 +-- libs/components/src/index.ts | 1 + 9 files changed, 152 insertions(+), 15 deletions(-) create mode 100644 libs/components/src/disclosure/disclosure-trigger-for.directive.ts create mode 100644 libs/components/src/disclosure/disclosure.component.ts create mode 100644 libs/components/src/disclosure/disclosure.mdx create mode 100644 libs/components/src/disclosure/disclosure.stories.ts create mode 100644 libs/components/src/disclosure/index.ts diff --git a/libs/components/src/disclosure/disclosure-trigger-for.directive.ts b/libs/components/src/disclosure/disclosure-trigger-for.directive.ts new file mode 100644 index 0000000000..0547028172 --- /dev/null +++ b/libs/components/src/disclosure/disclosure-trigger-for.directive.ts @@ -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; + } +} diff --git a/libs/components/src/disclosure/disclosure.component.ts b/libs/components/src/disclosure/disclosure.component.ts new file mode 100644 index 0000000000..58c67ad0f0 --- /dev/null +++ b/libs/components/src/disclosure/disclosure.component.ts @@ -0,0 +1,21 @@ +import { Component, HostBinding, Input, booleanAttribute } from "@angular/core"; + +let nextId = 0; + +@Component({ + selector: "bit-disclosure", + standalone: true, + template: ``, +}) +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++}`; +} diff --git a/libs/components/src/disclosure/disclosure.mdx b/libs/components/src/disclosure/disclosure.mdx new file mode 100644 index 0000000000..8df8e7025b --- /dev/null +++ b/libs/components/src/disclosure/disclosure.mdx @@ -0,0 +1,55 @@ +import { Meta, Story, Primary, Controls } from "@storybook/addon-docs"; + +import * as stories from "./disclosure.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. + +``` + +click button to hide this content +``` + + + +
+
+ +## 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. diff --git a/libs/components/src/disclosure/disclosure.stories.ts b/libs/components/src/disclosure/disclosure.stories.ts new file mode 100644 index 0000000000..974589a667 --- /dev/null +++ b/libs/components/src/disclosure/disclosure.stories.ts @@ -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; + +type Story = StoryObj; + +export const DisclosureWithIconButton: Story = { + render: (args) => ({ + props: args, + template: /*html*/ ` + + click button to hide this content + `, + }), +}; diff --git a/libs/components/src/disclosure/index.ts b/libs/components/src/disclosure/index.ts new file mode 100644 index 0000000000..b5bdf68725 --- /dev/null +++ b/libs/components/src/disclosure/index.ts @@ -0,0 +1,2 @@ +export * from "./disclosure-trigger-for.directive"; +export * from "./disclosure.component"; diff --git a/libs/components/src/icon-button/icon-button.component.ts b/libs/components/src/icon-button/icon-button.component.ts index 54f6dfda96..d036e1c77c 100644 --- a/libs/components/src/icon-button/icon-button.component.ts +++ b/libs/components/src/icon-button/icon-button.component.ts @@ -52,10 +52,14 @@ const styles: Record = { "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, diff --git a/libs/components/src/icon-button/icon-button.mdx b/libs/components/src/icon-button/icon-button.mdx index 8361d4c399..a45160d788 100644 --- a/libs/components/src/icon-button/icon-button.mdx +++ b/libs/components/src/icon-button/icon-button.mdx @@ -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). - - ## Styles 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 theme’s main `background` - + ### Muted Used for low emphasis icon buttons appearing on the theme’s main `background` - + ### Contrast Used on a theme’s colored or contrasting backgrounds such as in the navigation or on toasts and banners. - + ### Danger Danger is used for “trash” actions throughout the experience, most commonly in the bottom right of the dialog component. - + ### Primary Used in place of the main button component if no text is used. This allows the button to display square. - + ### Secondary Used in place of the main button component if no text is used. This allows the button to display square. - + ### Light Used on a background that is dark in both light theme and dark theme. Example: end user navigation styles. - + **Note:** Main and contrast styles appear on backgrounds where using `primary-700` as a focus 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 - + ### Default - + ## Accessibility diff --git a/libs/components/src/icon-button/icon-button.stories.ts b/libs/components/src/icon-button/icon-button.stories.ts index 0f25d2de58..b5542f7860 100644 --- a/libs/components/src/icon-button/icon-button.stories.ts +++ b/libs/components/src/icon-button/icon-button.stories.ts @@ -23,7 +23,7 @@ type Story = StoryObj; export const Default: Story = { render: (args) => ({ props: args, - template: ` + template: /*html*/ `
@@ -56,7 +56,7 @@ export const Small: Story = { export const Primary: Story = { render: (args) => ({ props: args, - template: ` + template: /*html*/ ` `, }), @@ -96,7 +96,7 @@ export const Muted: Story = { export const Light: Story = { render: (args) => ({ props: args, - template: ` + template: /*html*/ `
@@ -110,7 +110,7 @@ export const Light: Story = { export const Contrast: Story = { render: (args) => ({ props: args, - template: ` + template: /*html*/ `
diff --git a/libs/components/src/index.ts b/libs/components/src/index.ts index 6881d801e0..810f32bdd3 100644 --- a/libs/components/src/index.ts +++ b/libs/components/src/index.ts @@ -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";