[EC-457] Component library icon buttons (#3372)

* [EC-457] feat: initial version of icon button

* [EC-457] feat: modify template and start adding inputs

* [EC-457] feat: implement all styles

* [EC-457] chore: cleanup

* [EC-457] feat: fix hover styles after discussions

* [EC-457] feat: add focus ring workaround

* [EC-457] chore: refactor stories a bit

* [EC-457] fix: button style attr name reserved word collision

* [EC-356] feat: match padding with figma

* [EC-457] feat: use icon button in banner

* [EC-457] chore: cleanup css classes

* [EC-457] feat: improve aria

* [EC-457] feat: use icon button in dialog

* [EC-457] fix: make focus and hover styles independent

* [EC-457] fix: remove primary 500 border

* [EC-457] chore: cleanup

* [EC-457] chore: move css class to common list

* [EC-457] fix: use focus-visible

* [EC-457] chore: expand on workaround fix

* [EC-457] fix: default sizing

* [EC-457] fix: align trash icon right

* [EC-457] fix: add missing aria labels

* [EC-457] fix: add i18n service to banner tests

* [EC-457] chore: rename size `default` to `button`

* [EC-457] feat: double padding

* [EC-457] feat: simplify sizes - update default

* [EC-457] fix: revert selector fix - gonna create separate pr

* [EC-457] chore: remove superfluous dependencies

* [EC-457] fix: remove non-working onClose handler

Removing this storybook action because we already test it as part of the dialog service stories.
It requries mocking the dialogRef to capture the close function which makes this story more complex
but adds very little value as we already test it elsewhere.
This commit is contained in:
Andreas Coroiu 2022-09-15 07:38:29 +02:00 committed by GitHub
parent d2065cc91f
commit d666d66886
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 293 additions and 36 deletions

View File

@ -1,5 +1,5 @@
<div
class="tw-flex tw-items-center tw-gap-2 tw-py-2.5 tw-px-4 tw-text-contrast"
class="tw-flex tw-items-center tw-gap-2 tw-py-2.5 tw-px-4 tw-pr-2.5 tw-text-contrast"
[ngClass]="bannerClass"
[attr.role]="useAlertRole ? 'status' : null"
[attr.aria-live]="useAlertRole ? 'polite' : null"
@ -8,7 +8,12 @@
<span class="tw-grow tw-text-base">
<ng-content></ng-content>
</span>
<button class="tw-border-0 tw-bg-transparent tw-p-0 tw-text-contrast" (click)="onClose.emit()">
<i class="bwi bwi-close tw-text-sm" *ngIf="icon" aria-hidden="true"></i>
</button>
<button
bitIconButton="bwi-close"
buttonType="contrast"
size="default"
(click)="onClose.emit()"
[attr.title]="'close' | i18n"
[attr.aria-label]="'close' | i18n"
></button>
</div>

View File

@ -1,5 +1,10 @@
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { SharedModule } from "../shared/shared.module";
import { I18nMockService } from "../utils/i18n-mock.service";
import { BannerComponent } from "./banner.component";
describe("BannerComponent", () => {
@ -8,7 +13,17 @@ describe("BannerComponent", () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [SharedModule],
declarations: [BannerComponent],
providers: [
{
provide: I18nService,
useFactory: () =>
new I18nMockService({
close: "Close",
}),
},
],
}).compileComponents();
fixture = TestBed.createComponent(BannerComponent);

View File

@ -1,10 +1,13 @@
import { CommonModule } from "@angular/common";
import { NgModule } from "@angular/core";
import { IconButtonModule } from "../icon-button";
import { SharedModule } from "../shared/shared.module";
import { BannerComponent } from "./banner.component";
@NgModule({
imports: [CommonModule],
imports: [CommonModule, SharedModule, IconButtonModule],
exports: [BannerComponent],
declarations: [BannerComponent],
})

View File

@ -1,19 +1,43 @@
import { Meta, Story } from "@storybook/angular";
import { Meta, moduleMetadata, Story } from "@storybook/angular";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { IconButtonModule } from "../icon-button";
import { SharedModule } from "../shared/shared.module";
import { I18nMockService } from "../utils/i18n-mock.service";
import { BannerComponent } from "./banner.component";
export default {
title: "Component Library/Banner",
component: BannerComponent,
decorators: [
moduleMetadata({
imports: [SharedModule, IconButtonModule],
providers: [
{
provide: I18nService,
useFactory: () => {
return new I18nMockService({
close: "Close",
});
},
},
],
}),
],
args: {
bannerType: "warning",
},
argTypes: {
onClose: { action: "onClose" },
},
} as Meta;
const Template: Story<BannerComponent> = (args: BannerComponent) => ({
props: args,
template: `
<bit-banner [bannerType]="bannerType">Content Really Long Text Lorem Ipsum Ipsum Ipsum <button>Button</button></bit-banner>
<bit-banner [bannerType]="bannerType" (onClose)="onClose($event)">Content Really Long Text Lorem Ipsum Ipsum Ipsum <button>Button</button></bit-banner>
`,
});

View File

@ -9,8 +9,6 @@ const buttonStyles: Record<ButtonTypes, string[]> = {
"!tw-text-contrast",
"hover:tw-bg-primary-700",
"hover:tw-border-primary-700",
"focus:tw-bg-primary-700",
"focus:tw-border-primary-700",
"disabled:tw-bg-primary-500/60",
"disabled:tw-border-primary-500/60",
"disabled:!tw-text-contrast/60",
@ -23,9 +21,6 @@ const buttonStyles: Record<ButtonTypes, string[]> = {
"hover:tw-bg-secondary-500",
"hover:tw-border-secondary-500",
"hover:!tw-text-contrast",
"focus:tw-bg-secondary-500",
"focus:tw-border-secondary-500",
"focus:!tw-text-contrast",
"disabled:tw-bg-transparent",
"disabled:tw-border-text-muted/60",
"disabled:!tw-text-muted/60",
@ -37,9 +32,6 @@ const buttonStyles: Record<ButtonTypes, string[]> = {
"hover:tw-bg-danger-500",
"hover:tw-border-danger-500",
"hover:!tw-text-contrast",
"focus:tw-bg-danger-500",
"focus:tw-border-danger-500",
"focus:!tw-text-contrast",
"disabled:tw-bg-transparent",
"disabled:tw-border-danger-500/60",
"disabled:!tw-text-danger/60",
@ -62,10 +54,10 @@ export class ButtonDirective {
"tw-text-center",
"hover:tw-no-underline",
"focus:tw-outline-none",
"focus:tw-ring",
"focus:tw-ring-offset-2",
"focus:tw-ring-primary-700",
"focus:tw-z-10",
"focus-visible:tw-ring",
"focus-visible:tw-ring-offset-2",
"focus-visible:tw-ring-primary-700",
"focus-visible:tw-z-10",
]
.concat(this.block ? ["tw-w-full", "tw-block"] : ["tw-inline-block"])
.concat(buttonStyles[this.buttonType ?? "secondary"]);

View File

@ -1,6 +1,7 @@
import { DialogModule as CdkDialogModule } from "@angular/cdk/dialog";
import { NgModule } from "@angular/core";
import { IconButtonModule } from "../icon-button";
import { SharedModule } from "../shared";
import { DialogService } from "./dialog.service";
@ -10,11 +11,11 @@ import { DialogTitleContainerDirective } from "./directives/dialog-title-contain
import { SimpleDialogComponent } from "./simple-dialog/simple-dialog.component";
@NgModule({
imports: [SharedModule, CdkDialogModule],
imports: [SharedModule, IconButtonModule, CdkDialogModule],
declarations: [
DialogCloseDirective,
DialogComponent,
DialogTitleContainerDirective,
DialogComponent,
SimpleDialogComponent,
],
exports: [CdkDialogModule, DialogComponent, SimpleDialogComponent],

View File

@ -3,19 +3,19 @@
class="tw-my-4 tw-flex tw-max-h-screen tw-flex-col tw-overflow-hidden tw-rounded tw-border tw-border-solid tw-border-secondary-300 tw-bg-text-contrast tw-text-main"
>
<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-items-center tw-gap-4 tw-border-0 tw-border-b tw-border-solid tw-border-secondary-300 tw-p-4"
>
<h1 bitDialogTitleContainer class="tw-mb-0 tw-grow tw-text-lg tw-uppercase">
<ng-content select="[bitDialogTitle]"></ng-content>
</h1>
<button
bitIconButton="bwi-close"
buttonType="main"
size="default"
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>
[attr.title]="'close' | i18n"
[attr.aria-label]="'close' | i18n"
></button>
</div>
<div class="tw-overflow-y-auto tw-p-4 tw-pb-8">

View File

@ -3,6 +3,7 @@ import { Meta, moduleMetadata, Story } from "@storybook/angular";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { ButtonModule } from "../../button";
import { IconButtonModule } from "../../icon-button";
import { SharedModule } from "../../shared";
import { I18nMockService } from "../../utils/i18n-mock.service";
import { DialogCloseDirective } from "../directives/dialog-close.directive";
@ -15,7 +16,7 @@ export default {
component: DialogComponent,
decorators: [
moduleMetadata({
imports: [SharedModule, ButtonModule],
imports: [ButtonModule, SharedModule, IconButtonModule],
declarations: [DialogTitleContainerDirective, DialogCloseDirective],
providers: [
{
@ -46,9 +47,16 @@ const Template: Story<DialogComponent> = (args: DialogComponent) => ({
<bit-dialog [dialogSize]="dialogSize">
<span bitDialogTitle>{{title}}</span>
<span bitDialogContent>Dialog body text goes here.</span>
<div bitDialogFooter class="tw-flex tw-flex-row tw-gap-2">
<div bitDialogFooter class="tw-flex tw-items-center tw-flex-row tw-gap-2">
<button bitButton buttonType="primary">Save</button>
<button bitButton buttonType="secondary">Cancel</button>
<button
class="tw-ml-auto"
bitIconButton="bwi-trash"
buttonType="danger"
size="default"
title="Delete"
aria-label="Delete"></button>
</div>
</bit-dialog>
`,

View File

@ -2,7 +2,12 @@ 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 { IconButtonModule } from "../icon-button";
import { SharedModule } from "../shared/shared.module";
import { I18nMockService } from "../utils/i18n-mock.service";
import { DialogService } from "./dialog.service";
import { DialogCloseDirective } from "./directives/dialog-close.directive";
@ -60,13 +65,23 @@ export default {
decorators: [
moduleMetadata({
declarations: [
DialogCloseDirective,
SimpleDialogComponent,
DialogTitleContainerDirective,
StoryDialogContentComponent,
DialogCloseDirective,
DialogTitleContainerDirective,
SimpleDialogComponent,
],
imports: [SharedModule, IconButtonModule, ButtonModule, DialogModule],
providers: [
DialogService,
{
provide: I18nService,
useFactory: () => {
return new I18nMockService({
close: "Close",
});
},
},
],
imports: [ButtonModule, DialogModule],
providers: [DialogService],
}),
],
parameters: {

View File

@ -0,0 +1,115 @@
import { Component, HostBinding, Input } from "@angular/core";
export type IconButtonStyle = "contrast" | "main" | "muted" | "primary" | "secondary" | "danger";
const styles: Record<IconButtonStyle, string[]> = {
contrast: [
"tw-bg-transparent",
"!tw-text-contrast",
"tw-border-transparent",
"hover:tw-bg-transparent-hover",
"hover:tw-border-text-contrast",
"focus-visible:before:tw-ring-text-contrast",
"disabled:hover:tw-bg-transparent",
],
main: [
"tw-bg-transparent",
"!tw-text-main",
"tw-border-transparent",
"hover:tw-bg-transparent-hover",
"hover:tw-border-text-main",
"focus-visible:before:tw-ring-text-main",
"disabled:hover:tw-bg-transparent",
],
muted: [
"tw-bg-transparent",
"!tw-text-muted",
"tw-border-transparent",
"hover:tw-bg-transparent-hover",
"hover:tw-border-primary-700",
"focus-visible:before:tw-ring-primary-700",
"disabled:hover:tw-bg-transparent",
],
primary: [
"tw-bg-primary-500",
"!tw-text-contrast",
"tw-border-primary-500",
"hover:tw-bg-primary-700",
"hover:tw-border-primary-700",
"focus-visible:before:tw-ring-primary-700",
"disabled:hover:tw-bg-primary-500",
],
secondary: [
"tw-bg-transparent",
"!tw-text-muted",
"tw-border-text-muted",
"hover:!tw-text-contrast",
"hover:tw-bg-text-muted",
"focus-visible:before:tw-ring-primary-700",
"disabled:hover:tw-bg-transparent",
"disabled:hover:!tw-text-muted",
"disabled:hover:tw-border-text-muted",
],
danger: [
"tw-bg-transparent",
"!tw-text-danger",
"tw-border-danger-500",
"hover:!tw-text-contrast",
"hover:tw-bg-danger-500",
"focus-visible:before:tw-ring-primary-700",
"disabled:hover:tw-bg-transparent",
"disabled:hover:!tw-text-danger",
"disabled:hover:tw-border-danger-500",
],
};
export type IconButtonSize = "default" | "small";
const sizes: Record<IconButtonSize, string[]> = {
default: ["tw-px-2.5", "tw-py-1.5"],
small: ["tw-leading-none", "tw-text-base", "tw-p-1"],
};
@Component({
selector: "button[bitIconButton]",
template: `<i class="bwi" [ngClass]="icon" aria-hidden="true"></i>`,
})
export class BitIconButtonComponent {
@Input("bitIconButton") icon: string;
@Input() buttonType: IconButtonStyle = "main";
@Input() size: IconButtonSize = "default";
@HostBinding("class") get classList() {
return [
"tw-font-semibold",
"tw-border",
"tw-border-solid",
"tw-rounded",
"tw-transition",
"hover:tw-no-underline",
"disabled:tw-opacity-60",
"disabled:hover:tw-border-transparent",
"focus:tw-outline-none",
// Workaround for box-shadow with transparent offset issue:
// https://github.com/tailwindlabs/tailwindcss/issues/3595
// Remove `before:` and use regular `tw-ring` when browser no longer has bug, or better:
// switch to `outline` with `outline-offset` when Safari supports border radius on outline.
// Using `box-shadow` to create outlines is a hack and as such `outline` should be preferred.
"tw-relative",
"before:tw-content-['']",
"before:tw-block",
"before:tw-absolute",
"before:-tw-inset-[3px]",
"before:tw-rounded-md",
"before:tw-transition",
"before:tw-ring",
"focus-visible:before:tw-ring-text-contrast",
"focus-visible:tw-z-10",
]
.concat(styles[this.buttonType])
.concat(sizes[this.size]);
}
}

View File

@ -0,0 +1,11 @@
import { CommonModule } from "@angular/common";
import { NgModule } from "@angular/core";
import { BitIconButtonComponent } from "./icon-button.component";
@NgModule({
imports: [CommonModule],
declarations: [BitIconButtonComponent],
exports: [BitIconButtonComponent],
})
export class IconButtonModule {}

View File

@ -0,0 +1,59 @@
import { Meta, Story } from "@storybook/angular";
import { BitIconButtonComponent } from "./icon-button.component";
export default {
title: "Component Library/Icon Button",
component: BitIconButtonComponent,
args: {
bitIconButton: "bwi-plus",
buttonType: "primary",
size: "default",
disabled: false,
},
} as Meta;
const Template: Story<BitIconButtonComponent> = (args: BitIconButtonComponent) => ({
props: args,
template: `
<div class="tw-p-5" [class.tw-bg-primary-500]="buttonType === 'contrast'">
<button
[bitIconButton]="bitIconButton"
[buttonType]="buttonType"
[size]="size"
[disabled]="disabled"
title="Example icon button"
aria-label="Example icon button"></button>
</div>
`,
});
export const Contrast = Template.bind({});
Contrast.args = {
buttonType: "contrast",
};
export const Main = Template.bind({});
Main.args = {
buttonType: "main",
};
export const Muted = Template.bind({});
Muted.args = {
buttonType: "muted",
};
export const Primary = Template.bind({});
Primary.args = {
buttonType: "primary",
};
export const Secondary = Template.bind({});
Secondary.args = {
buttonType: "secondary",
};
export const Danger = Template.bind({});
Danger.args = {
buttonType: "danger",
};

View File

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

View File

@ -4,6 +4,7 @@ export * from "./button";
export * from "./callout";
export * from "./form-field";
export * from "./icon";
export * from "./icon-button";
export * from "./menu";
export * from "./dialog";
export * from "./submit-button";

View File

@ -1,4 +1,6 @@
:root {
--color-transparent-hover: rgb(0 0 0 / 0.03);
--color-background: 255 255 255;
--color-background-alt: 251 251 251;
--color-background-alt2: 23 92 219;
@ -37,6 +39,8 @@
}
.theme_dark {
--color-transparent-hover: rgb(255 255 255 / 0.12);
--color-background: 31 36 46;
--color-background-alt: 22 28 38;
--color-background-alt2: 47 52 61;

View File

@ -12,7 +12,10 @@ module.exports = {
corePlugins: { preflight: false },
theme: {
colors: {
transparent: colors.transparent,
transparent: {
DEFAULT: colors.transparent,
hover: "var(--color-transparent-hover)",
},
current: colors.current,
black: colors.black,
primary: {