[SM-154] Add labelledby to dialogs (#3439)

This commit is contained in:
Oscar Hinton 2022-09-08 10:37:23 +02:00 committed by GitHub
parent 05ebca2c4c
commit cb31a71e8d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 239 additions and 67 deletions

View File

@ -1,15 +1,22 @@
import { DialogModule as CdkDialogModule } from "@angular/cdk/dialog"; import { DialogModule as CdkDialogModule } from "@angular/cdk/dialog";
import { CommonModule } from "@angular/common";
import { NgModule } from "@angular/core"; import { NgModule } from "@angular/core";
import { DialogCloseDirective } from "./dialog-close.directive"; import { SharedModule } from "../shared";
import { DialogService } from "./dialog.service"; import { DialogService } from "./dialog.service";
import { DialogComponent } from "./dialog/dialog.component"; import { DialogComponent } from "./dialog/dialog.component";
import { DialogCloseDirective } from "./directives/dialog-close.directive";
import { DialogTitleContainerDirective } from "./directives/dialog-title-container.directive";
import { SimpleDialogComponent } from "./simple-dialog/simple-dialog.component"; import { SimpleDialogComponent } from "./simple-dialog/simple-dialog.component";
@NgModule({ @NgModule({
imports: [CommonModule, CdkDialogModule], imports: [SharedModule, CdkDialogModule],
declarations: [DialogCloseDirective, DialogComponent, SimpleDialogComponent], declarations: [
DialogCloseDirective,
DialogComponent,
DialogTitleContainerDirective,
SimpleDialogComponent,
],
exports: [CdkDialogModule, DialogComponent, SimpleDialogComponent], exports: [CdkDialogModule, DialogComponent, SimpleDialogComponent],
providers: [DialogService], providers: [DialogService],
}) })

View File

@ -0,0 +1,97 @@
import { DialogModule, DialogRef, DIALOG_DATA } from "@angular/cdk/dialog";
import { Component, Inject } from "@angular/core";
import { Meta, moduleMetadata, Story } from "@storybook/angular";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { ButtonModule } from "../button";
import { I18nMockService } from "../utils/i18n-mock.service";
import { DialogService } from "./dialog.service";
import { DialogComponent } from "./dialog/dialog.component";
import { DialogCloseDirective } from "./directives/dialog-close.directive";
import { DialogTitleContainerDirective } from "./directives/dialog-title-container.directive";
interface Animal {
animal: string;
}
@Component({
selector: "app-story-dialog",
template: `<button bitButton (click)="openDialog()">Open Dialog</button>`,
})
class StoryDialogComponent {
constructor(public dialogService: DialogService) {}
openDialog() {
this.dialogService.open(StoryDialogContentComponent, {
data: {
animal: "panda",
},
});
}
}
@Component({
selector: "story-dialog-content",
template: `
<bit-dialog [dialogSize]="large">
<span bitDialogTitle>Dialog Title</span>
<span bitDialogContent>
Dialog body text goes here.
<br />
Animal: {{ animal }}
</span>
<div bitDialogFooter class="tw-flex tw-flex-row tw-gap-2">
<button bitButton buttonType="primary" (click)="dialogRef.close()">Save</button>
<button bitButton buttonType="secondary" bitDialogClose>Cancel</button>
</div>
</bit-dialog>
`,
})
class StoryDialogContentComponent {
constructor(public dialogRef: DialogRef, @Inject(DIALOG_DATA) private data: Animal) {}
get animal() {
return this.data?.animal;
}
}
export default {
title: "Component Library/Dialogs/Service",
component: StoryDialogComponent,
decorators: [
moduleMetadata({
declarations: [
DialogCloseDirective,
DialogComponent,
DialogTitleContainerDirective,
StoryDialogContentComponent,
],
imports: [ButtonModule, DialogModule],
providers: [
DialogService,
{
provide: I18nService,
useFactory: () => {
return new I18nMockService({
close: "Close",
});
},
},
],
}),
],
parameters: {
design: {
type: "figma",
url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library",
},
},
} as Meta;
const Template: Story<StoryDialogComponent> = (args: StoryDialogComponent) => ({
props: args,
});
export const Default = Template.bind({});

View File

@ -5,21 +5,26 @@
<div <div
class="tw-flex tw-gap-4 tw-border-0 tw-border-b tw-border-solid tw-border-secondary-300 tw-p-4" class="tw-flex tw-gap-4 tw-border-0 tw-border-b tw-border-solid tw-border-secondary-300 tw-p-4"
> >
<h2 class="tw-mb-0 tw-grow tw-text-lg tw-uppercase"> <h1 bitDialogTitleContainer class="tw-mb-0 tw-grow tw-text-lg tw-uppercase">
<ng-content select="[bit-dialog-title]"></ng-content> <ng-content select="[bitDialogTitle]"></ng-content>
</h2> </h1>
<button class="tw-border-0 tw-bg-transparent tw-p-0" bitDialogClose> <button
bitDialogClose
class="tw-border-0 tw-bg-transparent tw-p-0"
title="{{ 'close' | i18n }}"
attr.aria-label="{{ 'close' | i18n }}"
>
<i class="bwi bwi-close tw-text-xs tw-font-bold tw-text-main" aria-hidden="true"></i> <i class="bwi bwi-close tw-text-xs tw-font-bold tw-text-main" aria-hidden="true"></i>
</button> </button>
</div> </div>
<div class="tw-overflow-y-auto tw-p-4 tw-pb-8"> <div class="tw-overflow-y-auto tw-p-4 tw-pb-8">
<ng-content select="[bit-dialog-content]"></ng-content> <ng-content select="[bitDialogContent]"></ng-content>
</div> </div>
<div <div
class="tw-border-0 tw-border-t tw-border-solid tw-border-secondary-300 tw-bg-background-alt tw-p-4" class="tw-border-0 tw-border-t tw-border-solid tw-border-secondary-300 tw-bg-background-alt tw-p-4"
> >
<ng-content select="[bit-dialog-footer]"></ng-content> <ng-content select="[bitDialogFooter]"></ng-content>
</div> </div>
</div> </div>

View File

@ -1,6 +1,12 @@
import { Meta, moduleMetadata, Story } from "@storybook/angular"; import { Meta, moduleMetadata, Story } from "@storybook/angular";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { ButtonModule } from "../../button"; import { ButtonModule } from "../../button";
import { SharedModule } from "../../shared";
import { I18nMockService } from "../../utils/i18n-mock.service";
import { DialogCloseDirective } from "../directives/dialog-close.directive";
import { DialogTitleContainerDirective } from "../directives/dialog-title-container.directive";
import { DialogComponent } from "./dialog.component"; import { DialogComponent } from "./dialog.component";
@ -9,7 +15,18 @@ export default {
component: DialogComponent, component: DialogComponent,
decorators: [ decorators: [
moduleMetadata({ moduleMetadata({
imports: [ButtonModule], imports: [SharedModule, ButtonModule],
declarations: [DialogTitleContainerDirective, DialogCloseDirective],
providers: [
{
provide: I18nService,
useFactory: () => {
return new I18nMockService({
close: "Close",
});
},
},
],
}), }),
], ],
args: { args: {
@ -27,9 +44,9 @@ const Template: Story<DialogComponent> = (args: DialogComponent) => ({
props: args, props: args,
template: ` template: `
<bit-dialog [dialogSize]="dialogSize"> <bit-dialog [dialogSize]="dialogSize">
<span bit-dialog-title>{{title}}</span> <span bitDialogTitle>{{title}}</span>
<span bit-dialog-content>Dialog body text goes here.</span> <span bitDialogContent>Dialog body text goes here.</span>
<div bit-dialog-footer class="tw-flex tw-flex-row tw-gap-2"> <div bitDialogFooter class="tw-flex tw-flex-row tw-gap-2">
<button bitButton buttonType="primary">Save</button> <button bitButton buttonType="primary">Save</button>
<button bitButton buttonType="secondary">Cancel</button> <button bitButton buttonType="secondary">Cancel</button>
</div> </div>
@ -59,15 +76,15 @@ const TemplateScrolling: Story<DialogComponent> = (args: DialogComponent) => ({
props: args, props: args,
template: ` template: `
<bit-dialog [dialogSize]="dialogSize"> <bit-dialog [dialogSize]="dialogSize">
<span bit-dialog-title>Scrolling Example</span> <span bitDialogTitle>Scrolling Example</span>
<span bit-dialog-content> <span bitDialogContent>
Dialog body text goes here.<br> Dialog body text goes here.<br>
<ng-container *ngFor="let _ of [].constructor(100)"> <ng-container *ngFor="let _ of [].constructor(100)">
repeating lines of characters <br> repeating lines of characters <br>
</ng-container> </ng-container>
end of sequence! end of sequence!
</span> </span>
<div bit-dialog-footer class="tw-flex tw-flex-row tw-gap-2"> <div bitDialogFooter class="tw-flex tw-flex-row tw-gap-2">
<button bitButton buttonType="primary">Save</button> <button bitButton buttonType="primary">Save</button>
<button bitButton buttonType="secondary">Cancel</button> <button bitButton buttonType="secondary">Cancel</button>
</div> </div>

View File

@ -1,18 +1,15 @@
import { DialogRef } from "@angular/cdk/dialog"; import { DialogRef } from "@angular/cdk/dialog";
import { Directive, Input, Optional } from "@angular/core"; import { Directive, HostListener, Input, Optional } from "@angular/core";
@Directive({ @Directive({
selector: "[bitDialogClose]", selector: "[bitDialogClose]",
host: {
"(click)": "close()",
},
}) })
export class DialogCloseDirective { export class DialogCloseDirective {
@Input("bit-dialog-close") dialogResult: any; @Input("bit-dialog-close") dialogResult: any;
constructor(@Optional() public dialogRef: DialogRef<any>) {} constructor(@Optional() public dialogRef: DialogRef<any>) {}
close() { @HostListener("click") close(): void {
this.dialogRef.close(this.dialogResult); this.dialogRef.close(this.dialogResult);
} }
} }

View File

@ -0,0 +1,30 @@
import { CdkDialogContainer, DialogRef } from "@angular/cdk/dialog";
import { Directive, HostBinding, Input, OnInit, Optional } from "@angular/core";
// Increments for each instance of this component
let nextId = 0;
@Directive({
selector: "[bitDialogTitleContainer]",
})
export class DialogTitleContainerDirective implements OnInit {
@HostBinding("id") id = `bit-dialog-title-${nextId++}`;
@Input() simple = false;
constructor(@Optional() private dialogRef: DialogRef<any>) {}
ngOnInit(): void {
// Based on angular/components, licensed under MIT
// https://github.com/angular/components/blob/14.2.0/src/material/dialog/dialog-content-directives.ts#L121-L128
if (this.dialogRef) {
Promise.resolve().then(() => {
const container = this.dialogRef.containerInstance as CdkDialogContainer;
if (container && !container._ariaLabelledBy) {
container._ariaLabelledBy = this.id;
}
});
}
}
}

View File

@ -4,9 +4,10 @@ import { Meta, moduleMetadata, Story } from "@storybook/angular";
import { ButtonModule } from "../button"; import { ButtonModule } from "../button";
import { DialogCloseDirective } from "./dialog-close.directive";
import { DialogService } from "./dialog.service"; import { DialogService } from "./dialog.service";
import { DialogComponent } from "./dialog/dialog.component"; import { DialogCloseDirective } from "./directives/dialog-close.directive";
import { DialogTitleContainerDirective } from "./directives/dialog-title-container.directive";
import { SimpleDialogComponent } from "./simple-dialog/simple-dialog.component";
interface Animal { interface Animal {
animal: string; animal: string;
@ -14,7 +15,7 @@ interface Animal {
@Component({ @Component({
selector: "app-story-dialog", selector: "app-story-dialog",
template: `<button bitButton (click)="openDialog()">Open Dialog</button>`, template: `<button bitButton (click)="openDialog()">Open Simple Dialog</button>`,
}) })
class StoryDialogComponent { class StoryDialogComponent {
constructor(public dialogService: DialogService) {} constructor(public dialogService: DialogService) {}
@ -31,18 +32,18 @@ class StoryDialogComponent {
@Component({ @Component({
selector: "story-dialog-content", selector: "story-dialog-content",
template: ` template: `
<bit-dialog [dialogSize]="large"> <bit-simple-dialog>
<span bit-dialog-title>Dialog Title</span> <span bitDialogTitle>Dialog Title</span>
<span bit-dialog-content> <span bitDialogContent>
Dialog body text goes here. Dialog body text goes here.
<br /> <br />
Animal: {{ animal }} Animal: {{ animal }}
</span> </span>
<div bit-dialog-footer class="tw-flex tw-flex-row tw-gap-2"> <div bitDialogFooter class="tw-flex tw-flex-row tw-gap-2">
<button bitButton buttonType="primary" (click)="dialogRef.close()">Save</button> <button bitButton buttonType="primary" (click)="dialogRef.close()">Save</button>
<button bitButton buttonType="secondary" bitDialogClose>Cancel</button> <button bitButton buttonType="secondary" bitDialogClose>Cancel</button>
</div> </div>
</bit-dialog> </bit-simple-dialog>
`, `,
}) })
class StoryDialogContentComponent { class StoryDialogContentComponent {
@ -54,11 +55,16 @@ class StoryDialogContentComponent {
} }
export default { export default {
title: "Component Library/Dialogs/Service", title: "Component Library/Dialogs/Service/Simple",
component: StoryDialogComponent, component: StoryDialogComponent,
decorators: [ decorators: [
moduleMetadata({ moduleMetadata({
declarations: [DialogComponent, StoryDialogContentComponent, DialogCloseDirective], declarations: [
DialogCloseDirective,
SimpleDialogComponent,
DialogTitleContainerDirective,
StoryDialogContentComponent,
],
imports: [ButtonModule, DialogModule], imports: [ButtonModule, DialogModule],
providers: [DialogService], providers: [DialogService],
}), }),

View File

@ -6,14 +6,14 @@
<ng-template #elseBlock> <ng-template #elseBlock>
<i class="bwi bwi-exclamation-triangle tw-text-3xl tw-text-warning" aria-hidden="true"></i> <i class="bwi bwi-exclamation-triangle tw-text-3xl tw-text-warning" aria-hidden="true"></i>
</ng-template> </ng-template>
<h2 class="tw-mb-0 tw-text-base tw-font-semibold"> <h1 bitDialogTitleContainer class="tw-mb-0 tw-text-base tw-font-semibold">
<ng-content select="[bit-dialog-title]"></ng-content> <ng-content select="[bitDialogTitle]"></ng-content>
</h2> </h1>
</div> </div>
<div class="tw-overflow-y-auto tw-px-4 tw-pt-2 tw-pb-4 tw-text-center tw-text-base"> <div class="tw-overflow-y-auto tw-px-4 tw-pt-2 tw-pb-4 tw-text-center tw-text-base">
<ng-content select="[bit-dialog-content]"></ng-content> <ng-content select="[bitDialogContent]"></ng-content>
</div> </div>
<div class="tw-border-0 tw-border-t tw-border-solid tw-border-secondary-300 tw-p-4"> <div class="tw-border-0 tw-border-t tw-border-solid tw-border-secondary-300 tw-p-4">
<ng-content select="[bit-dialog-footer]"></ng-content> <ng-content select="[bitDialogFooter]"></ng-content>
</div> </div>
</div> </div>

View File

@ -1,6 +1,7 @@
import { Meta, moduleMetadata, Story } from "@storybook/angular"; import { Meta, moduleMetadata, Story } from "@storybook/angular";
import { ButtonModule } from "../../button"; import { ButtonModule } from "../../button";
import { DialogTitleContainerDirective } from "../directives/dialog-title-container.directive";
import { IconDirective, SimpleDialogComponent } from "./simple-dialog.component"; import { IconDirective, SimpleDialogComponent } from "./simple-dialog.component";
@ -10,7 +11,7 @@ export default {
decorators: [ decorators: [
moduleMetadata({ moduleMetadata({
imports: [ButtonModule], imports: [ButtonModule],
declarations: [IconDirective], declarations: [IconDirective, DialogTitleContainerDirective],
}), }),
], ],
parameters: { parameters: {
@ -25,9 +26,9 @@ const Template: Story<SimpleDialogComponent> = (args: SimpleDialogComponent) =>
props: args, props: args,
template: ` template: `
<bit-simple-dialog> <bit-simple-dialog>
<span bit-dialog-title>Alert Dialog</span> <span bitDialogTitle>Alert Dialog</span>
<span bit-dialog-content>Message Content</span> <span bitDialogContent>Message Content</span>
<div bit-dialog-footer class="tw-flex tw-flex-row tw-gap-2"> <div bitDialogFooter class="tw-flex tw-flex-row tw-gap-2">
<button bitButton buttonType="primary">Yes</button> <button bitButton buttonType="primary">Yes</button>
<button bitButton buttonType="secondary">No</button> <button bitButton buttonType="secondary">No</button>
</div> </div>
@ -42,9 +43,9 @@ const TemplateWithIcon: Story<SimpleDialogComponent> = (args: SimpleDialogCompon
template: ` template: `
<bit-simple-dialog> <bit-simple-dialog>
<i bit-dialog-icon class="bwi bwi-star tw-text-3xl tw-text-success" aria-hidden="true"></i> <i bit-dialog-icon class="bwi bwi-star tw-text-3xl tw-text-success" aria-hidden="true"></i>
<span bit-dialog-title>Premium Subscription Available</span> <span bitDialogTitle>Premium Subscription Available</span>
<span bit-dialog-content> Message Content</span> <span bitDialogContent> Message Content</span>
<div bit-dialog-footer class="tw-flex tw-flex-row tw-gap-2"> <div bitDialogFooter class="tw-flex tw-flex-row tw-gap-2">
<button bitButton buttonType="primary">Yes</button> <button bitButton buttonType="primary">Yes</button>
<button bitButton buttonType="secondary">No</button> <button bitButton buttonType="secondary">No</button>
</div> </div>
@ -58,8 +59,8 @@ const TemplateScroll: Story<SimpleDialogComponent> = (args: SimpleDialogComponen
props: args, props: args,
template: ` template: `
<bit-simple-dialog> <bit-simple-dialog>
<span bit-dialog-title>Alert Dialog</span> <span bitDialogTitle>Alert Dialog</span>
<span bit-dialog-content> <span bitDialogContent>
Message Content Message Content
Message text goes here.<br> Message text goes here.<br>
<ng-container *ngFor="let _ of [].constructor(100)"> <ng-container *ngFor="let _ of [].constructor(100)">
@ -67,7 +68,7 @@ const TemplateScroll: Story<SimpleDialogComponent> = (args: SimpleDialogComponen
</ng-container> </ng-container>
end of sequence! end of sequence!
</span> </span>
<div bit-dialog-footer class="tw-flex tw-flex-row tw-gap-2"> <div bitDialogFooter class="tw-flex tw-flex-row tw-gap-2">
<button bitButton buttonType="primary">Yes</button> <button bitButton buttonType="primary">Yes</button>
<button bitButton buttonType="secondary">No</button> <button bitButton buttonType="secondary">No</button>
</div> </div>

View File

@ -1,10 +1,8 @@
import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core";
import { NgModule, Pipe, PipeTransform } from "@angular/core";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { BitInputDirective } from "../input/input.directive"; import { BitInputDirective } from "../input/input.directive";
import { InputModule } from "../input/input.module"; import { InputModule } from "../input/input.module";
import { SharedModule } from "../shared";
import { BitErrorSummary } from "./error-summary.component"; import { BitErrorSummary } from "./error-summary.component";
import { BitErrorComponent } from "./error.component"; import { BitErrorComponent } from "./error.component";
@ -14,22 +12,8 @@ import { BitLabel } from "./label.directive";
import { BitPrefixDirective } from "./prefix.directive"; import { BitPrefixDirective } from "./prefix.directive";
import { BitSuffixDirective } from "./suffix.directive"; import { BitSuffixDirective } from "./suffix.directive";
/**
* Temporarily duplicate this pipe
*/
@Pipe({
name: "i18n",
})
export class I18nPipe implements PipeTransform {
constructor(private i18nService: I18nService) {}
transform(id: string, p1?: string, p2?: string, p3?: string): string {
return this.i18nService.t(id, p1, p2, p3);
}
}
@NgModule({ @NgModule({
imports: [CommonModule, InputModule], imports: [SharedModule, InputModule],
exports: [ exports: [
BitErrorComponent, BitErrorComponent,
BitErrorSummary, BitErrorSummary,
@ -48,7 +32,6 @@ export class I18nPipe implements PipeTransform {
BitLabel, BitLabel,
BitPrefixDirective, BitPrefixDirective,
BitSuffixDirective, BitSuffixDirective,
I18nPipe,
], ],
}) })
export class FormFieldModule {} export class FormFieldModule {}

View File

@ -0,0 +1,17 @@
import { Pipe, PipeTransform } from "@angular/core";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
/**
* Temporarily duplicate this pipe
*/
@Pipe({
name: "i18n",
})
export class I18nPipe implements PipeTransform {
constructor(private i18nService: I18nService) {}
transform(id: string, p1?: string, p2?: string, p3?: string): string {
return this.i18nService.t(id, p1, p2, p3);
}
}

View File

@ -0,0 +1 @@
export * from "./shared.module";

View File

@ -0,0 +1,11 @@
import { CommonModule } from "@angular/common";
import { NgModule } from "@angular/core";
import { I18nPipe } from "./i18n.pipe";
@NgModule({
imports: [CommonModule],
declarations: [I18nPipe],
exports: [CommonModule, I18nPipe],
})
export class SharedModule {}