[SM-154] Add labelledby to dialogs (#3439)
This commit is contained in:
parent
05ebca2c4c
commit
cb31a71e8d
|
@ -1,15 +1,22 @@
|
|||
import { DialogModule as CdkDialogModule } from "@angular/cdk/dialog";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { DialogCloseDirective } from "./dialog-close.directive";
|
||||
import { SharedModule } from "../shared";
|
||||
|
||||
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";
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, CdkDialogModule],
|
||||
declarations: [DialogCloseDirective, DialogComponent, SimpleDialogComponent],
|
||||
imports: [SharedModule, CdkDialogModule],
|
||||
declarations: [
|
||||
DialogCloseDirective,
|
||||
DialogComponent,
|
||||
DialogTitleContainerDirective,
|
||||
SimpleDialogComponent,
|
||||
],
|
||||
exports: [CdkDialogModule, DialogComponent, SimpleDialogComponent],
|
||||
providers: [DialogService],
|
||||
})
|
||||
|
|
|
@ -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({});
|
|
@ -5,21 +5,26 @@
|
|||
<div
|
||||
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">
|
||||
<ng-content select="[bit-dialog-title]"></ng-content>
|
||||
</h2>
|
||||
<button class="tw-border-0 tw-bg-transparent tw-p-0" bitDialogClose>
|
||||
<h1 bitDialogTitleContainer class="tw-mb-0 tw-grow tw-text-lg tw-uppercase">
|
||||
<ng-content select="[bitDialogTitle]"></ng-content>
|
||||
</h1>
|
||||
<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>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<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
|
||||
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>
|
||||
|
|
|
@ -1,6 +1,12 @@
|
|||
import { Meta, moduleMetadata, Story } from "@storybook/angular";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
|
||||
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";
|
||||
|
||||
|
@ -9,7 +15,18 @@ export default {
|
|||
component: DialogComponent,
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
imports: [ButtonModule],
|
||||
imports: [SharedModule, ButtonModule],
|
||||
declarations: [DialogTitleContainerDirective, DialogCloseDirective],
|
||||
providers: [
|
||||
{
|
||||
provide: I18nService,
|
||||
useFactory: () => {
|
||||
return new I18nMockService({
|
||||
close: "Close",
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
args: {
|
||||
|
@ -27,9 +44,9 @@ const Template: Story<DialogComponent> = (args: DialogComponent) => ({
|
|||
props: args,
|
||||
template: `
|
||||
<bit-dialog [dialogSize]="dialogSize">
|
||||
<span bit-dialog-title>{{title}}</span>
|
||||
<span bit-dialog-content>Dialog body text goes here.</span>
|
||||
<div bit-dialog-footer class="tw-flex tw-flex-row tw-gap-2">
|
||||
<span bitDialogTitle>{{title}}</span>
|
||||
<span bitDialogContent>Dialog body text goes here.</span>
|
||||
<div bitDialogFooter class="tw-flex tw-flex-row tw-gap-2">
|
||||
<button bitButton buttonType="primary">Save</button>
|
||||
<button bitButton buttonType="secondary">Cancel</button>
|
||||
</div>
|
||||
|
@ -59,15 +76,15 @@ const TemplateScrolling: Story<DialogComponent> = (args: DialogComponent) => ({
|
|||
props: args,
|
||||
template: `
|
||||
<bit-dialog [dialogSize]="dialogSize">
|
||||
<span bit-dialog-title>Scrolling Example</span>
|
||||
<span bit-dialog-content>
|
||||
<span bitDialogTitle>Scrolling Example</span>
|
||||
<span bitDialogContent>
|
||||
Dialog body text goes here.<br>
|
||||
<ng-container *ngFor="let _ of [].constructor(100)">
|
||||
repeating lines of characters <br>
|
||||
</ng-container>
|
||||
end of sequence!
|
||||
</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="secondary">Cancel</button>
|
||||
</div>
|
||||
|
|
|
@ -1,18 +1,15 @@
|
|||
import { DialogRef } from "@angular/cdk/dialog";
|
||||
import { Directive, Input, Optional } from "@angular/core";
|
||||
import { Directive, HostListener, Input, Optional } from "@angular/core";
|
||||
|
||||
@Directive({
|
||||
selector: "[bitDialogClose]",
|
||||
host: {
|
||||
"(click)": "close()",
|
||||
},
|
||||
})
|
||||
export class DialogCloseDirective {
|
||||
@Input("bit-dialog-close") dialogResult: any;
|
||||
|
||||
constructor(@Optional() public dialogRef: DialogRef<any>) {}
|
||||
|
||||
close() {
|
||||
@HostListener("click") close(): void {
|
||||
this.dialogRef.close(this.dialogResult);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,9 +4,10 @@ import { Meta, moduleMetadata, Story } from "@storybook/angular";
|
|||
|
||||
import { ButtonModule } from "../button";
|
||||
|
||||
import { DialogCloseDirective } from "./dialog-close.directive";
|
||||
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 {
|
||||
animal: string;
|
||||
|
@ -14,7 +15,7 @@ interface Animal {
|
|||
|
||||
@Component({
|
||||
selector: "app-story-dialog",
|
||||
template: `<button bitButton (click)="openDialog()">Open Dialog</button>`,
|
||||
template: `<button bitButton (click)="openDialog()">Open Simple Dialog</button>`,
|
||||
})
|
||||
class StoryDialogComponent {
|
||||
constructor(public dialogService: DialogService) {}
|
||||
|
@ -31,18 +32,18 @@ class StoryDialogComponent {
|
|||
@Component({
|
||||
selector: "story-dialog-content",
|
||||
template: `
|
||||
<bit-dialog [dialogSize]="large">
|
||||
<span bit-dialog-title>Dialog Title</span>
|
||||
<span bit-dialog-content>
|
||||
<bit-simple-dialog>
|
||||
<span bitDialogTitle>Dialog Title</span>
|
||||
<span bitDialogContent>
|
||||
Dialog body text goes here.
|
||||
<br />
|
||||
Animal: {{ animal }}
|
||||
</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="secondary" bitDialogClose>Cancel</button>
|
||||
</div>
|
||||
</bit-dialog>
|
||||
</bit-simple-dialog>
|
||||
`,
|
||||
})
|
||||
class StoryDialogContentComponent {
|
||||
|
@ -54,11 +55,16 @@ class StoryDialogContentComponent {
|
|||
}
|
||||
|
||||
export default {
|
||||
title: "Component Library/Dialogs/Service",
|
||||
title: "Component Library/Dialogs/Service/Simple",
|
||||
component: StoryDialogComponent,
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
declarations: [DialogComponent, StoryDialogContentComponent, DialogCloseDirective],
|
||||
declarations: [
|
||||
DialogCloseDirective,
|
||||
SimpleDialogComponent,
|
||||
DialogTitleContainerDirective,
|
||||
StoryDialogContentComponent,
|
||||
],
|
||||
imports: [ButtonModule, DialogModule],
|
||||
providers: [DialogService],
|
||||
}),
|
|
@ -6,14 +6,14 @@
|
|||
<ng-template #elseBlock>
|
||||
<i class="bwi bwi-exclamation-triangle tw-text-3xl tw-text-warning" aria-hidden="true"></i>
|
||||
</ng-template>
|
||||
<h2 class="tw-mb-0 tw-text-base tw-font-semibold">
|
||||
<ng-content select="[bit-dialog-title]"></ng-content>
|
||||
</h2>
|
||||
<h1 bitDialogTitleContainer class="tw-mb-0 tw-text-base tw-font-semibold">
|
||||
<ng-content select="[bitDialogTitle]"></ng-content>
|
||||
</h1>
|
||||
</div>
|
||||
<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 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>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { Meta, moduleMetadata, Story } from "@storybook/angular";
|
||||
|
||||
import { ButtonModule } from "../../button";
|
||||
import { DialogTitleContainerDirective } from "../directives/dialog-title-container.directive";
|
||||
|
||||
import { IconDirective, SimpleDialogComponent } from "./simple-dialog.component";
|
||||
|
||||
|
@ -10,7 +11,7 @@ export default {
|
|||
decorators: [
|
||||
moduleMetadata({
|
||||
imports: [ButtonModule],
|
||||
declarations: [IconDirective],
|
||||
declarations: [IconDirective, DialogTitleContainerDirective],
|
||||
}),
|
||||
],
|
||||
parameters: {
|
||||
|
@ -25,9 +26,9 @@ const Template: Story<SimpleDialogComponent> = (args: SimpleDialogComponent) =>
|
|||
props: args,
|
||||
template: `
|
||||
<bit-simple-dialog>
|
||||
<span bit-dialog-title>Alert Dialog</span>
|
||||
<span bit-dialog-content>Message Content</span>
|
||||
<div bit-dialog-footer class="tw-flex tw-flex-row tw-gap-2">
|
||||
<span bitDialogTitle>Alert Dialog</span>
|
||||
<span bitDialogContent>Message Content</span>
|
||||
<div bitDialogFooter class="tw-flex tw-flex-row tw-gap-2">
|
||||
<button bitButton buttonType="primary">Yes</button>
|
||||
<button bitButton buttonType="secondary">No</button>
|
||||
</div>
|
||||
|
@ -42,9 +43,9 @@ const TemplateWithIcon: Story<SimpleDialogComponent> = (args: SimpleDialogCompon
|
|||
template: `
|
||||
<bit-simple-dialog>
|
||||
<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 bit-dialog-content> Message Content</span>
|
||||
<div bit-dialog-footer class="tw-flex tw-flex-row tw-gap-2">
|
||||
<span bitDialogTitle>Premium Subscription Available</span>
|
||||
<span bitDialogContent> Message Content</span>
|
||||
<div bitDialogFooter class="tw-flex tw-flex-row tw-gap-2">
|
||||
<button bitButton buttonType="primary">Yes</button>
|
||||
<button bitButton buttonType="secondary">No</button>
|
||||
</div>
|
||||
|
@ -58,8 +59,8 @@ const TemplateScroll: Story<SimpleDialogComponent> = (args: SimpleDialogComponen
|
|||
props: args,
|
||||
template: `
|
||||
<bit-simple-dialog>
|
||||
<span bit-dialog-title>Alert Dialog</span>
|
||||
<span bit-dialog-content>
|
||||
<span bitDialogTitle>Alert Dialog</span>
|
||||
<span bitDialogContent>
|
||||
Message Content
|
||||
Message text goes here.<br>
|
||||
<ng-container *ngFor="let _ of [].constructor(100)">
|
||||
|
@ -67,7 +68,7 @@ const TemplateScroll: Story<SimpleDialogComponent> = (args: SimpleDialogComponen
|
|||
</ng-container>
|
||||
end of sequence!
|
||||
</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="secondary">No</button>
|
||||
</div>
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
import { CommonModule } from "@angular/common";
|
||||
import { NgModule, Pipe, PipeTransform } from "@angular/core";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { BitInputDirective } from "../input/input.directive";
|
||||
import { InputModule } from "../input/input.module";
|
||||
import { SharedModule } from "../shared";
|
||||
|
||||
import { BitErrorSummary } from "./error-summary.component";
|
||||
import { BitErrorComponent } from "./error.component";
|
||||
|
@ -14,22 +12,8 @@ import { BitLabel } from "./label.directive";
|
|||
import { BitPrefixDirective } from "./prefix.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({
|
||||
imports: [CommonModule, InputModule],
|
||||
imports: [SharedModule, InputModule],
|
||||
exports: [
|
||||
BitErrorComponent,
|
||||
BitErrorSummary,
|
||||
|
@ -48,7 +32,6 @@ export class I18nPipe implements PipeTransform {
|
|||
BitLabel,
|
||||
BitPrefixDirective,
|
||||
BitSuffixDirective,
|
||||
I18nPipe,
|
||||
],
|
||||
})
|
||||
export class FormFieldModule {}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export * from "./shared.module";
|
|
@ -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 {}
|
Loading…
Reference in New Issue