mirror of
https://github.com/bitwarden/browser
synced 2025-12-14 15:23:33 +00:00
[EC-558] Reflecting async progress on buttons and forms (#3548)
* [EC-556] feat: convert button into component * [EC-556] feat: implement loading state * [EC-556] feat: remove loading from submit button * [EC-556] fix: add missing import * [EC-556] fix: disabling button using regular attribute * [EC-556] feat: implement bitFormButton * [EC-556] feat: use bitFormButton in submit button * [EC-556] fix: missing import * [EC-558] chore: rename file to match class name * [EC-558] feat: allow skipping bitButton on form buttons * [EC-558]: only show spinner on submit button * [EC-558] feat: add new bit async directive * [EC-558] feat: add functionToObservable util * [EC-558] feat: implement bitAction directive * [EC-558] refactor: simplify bitSubmit using functionToObservable * [EC-558] feat: connect bit action with form button * [EC-558] feat: execute function immediately to allow for form validation * [EC-558] feat: disable form on loading * [EC-558] chore: remove duplicate types * [EC-558] feat: move validation service to common * [EC-558] feat: add error handling using validation service * [EC-558] feat: add support for icon button * [EC-558] fix: icon button hover border styles * [EC-558] chore: refactor icon button story to show all styles * [EC-558] fix: better align loading spinner to middle * [EC-558] fix: simplify try catch * [EC-558] chore: reorganize async actions * [EC-558] chore: rename stories * [EC-558] docs: add documentation * [EC-558] feat: decouple buttons and form buttons * [EC-558] chore: rename button like abstraction * [EC-558] chore: remove null check * [EC-558] docs: add jsdocs to directives * [EC-558] fix: switch abs imports to relative * [EC-558] chore: add async actions module to web shared module * [EC-558] chore: remove unecessary null check * [EC-558] chore: apply suggestions from code review Co-authored-by: Oscar Hinton <Hinton@users.noreply.github.com> * [EC-558] fix: whitespaces * [EC-558] feat: dont disable form by default * [EC-558] fix: bug where form could be submit during a previous submit * [EC-558] feat: remove ability to disable form Co-authored-by: Oscar Hinton <Hinton@users.noreply.github.com>
This commit is contained in:
15
libs/components/src/icon-button/icon-button.component.html
Normal file
15
libs/components/src/icon-button/icon-button.component.html
Normal file
@@ -0,0 +1,15 @@
|
||||
<span class="tw-relative">
|
||||
<span [ngClass]="{ 'tw-invisible': loading }">
|
||||
<i class="bwi" [ngClass]="iconClass" aria-hidden="true"></i>
|
||||
</span>
|
||||
<span
|
||||
class="tw-absolute tw-inset-0 tw-flex tw-items-center tw-justify-center"
|
||||
[ngClass]="{ 'tw-invisible': !loading }"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin"
|
||||
aria-hidden="true"
|
||||
[ngClass]="{ 'bwi-lg': size === 'default' }"
|
||||
></i>
|
||||
</span>
|
||||
</span>
|
||||
@@ -1,8 +1,10 @@
|
||||
import { Component, HostBinding, Input } from "@angular/core";
|
||||
|
||||
export type IconButtonStyle = "contrast" | "main" | "muted" | "primary" | "secondary" | "danger";
|
||||
import { ButtonLikeAbstraction } from "../shared/button-like.abstraction";
|
||||
|
||||
const styles: Record<IconButtonStyle, string[]> = {
|
||||
export type IconButtonType = "contrast" | "main" | "muted" | "primary" | "secondary" | "danger";
|
||||
|
||||
const styles: Record<IconButtonType, string[]> = {
|
||||
contrast: [
|
||||
"tw-bg-transparent",
|
||||
"!tw-text-contrast",
|
||||
@@ -10,6 +12,7 @@ const styles: Record<IconButtonStyle, string[]> = {
|
||||
"hover:tw-bg-transparent-hover",
|
||||
"hover:tw-border-text-contrast",
|
||||
"focus-visible:before:tw-ring-text-contrast",
|
||||
"disabled:hover:tw-border-transparent",
|
||||
"disabled:hover:tw-bg-transparent",
|
||||
],
|
||||
main: [
|
||||
@@ -19,6 +22,7 @@ const styles: Record<IconButtonStyle, string[]> = {
|
||||
"hover:tw-bg-transparent-hover",
|
||||
"hover:tw-border-text-main",
|
||||
"focus-visible:before:tw-ring-text-main",
|
||||
"disabled:hover:tw-border-transparent",
|
||||
"disabled:hover:tw-bg-transparent",
|
||||
],
|
||||
muted: [
|
||||
@@ -28,6 +32,7 @@ const styles: Record<IconButtonStyle, string[]> = {
|
||||
"hover:tw-bg-transparent-hover",
|
||||
"hover:tw-border-primary-700",
|
||||
"focus-visible:before:tw-ring-primary-700",
|
||||
"disabled:hover:tw-border-transparent",
|
||||
"disabled:hover:tw-bg-transparent",
|
||||
],
|
||||
primary: [
|
||||
@@ -37,6 +42,7 @@ const styles: Record<IconButtonStyle, string[]> = {
|
||||
"hover:tw-bg-primary-700",
|
||||
"hover:tw-border-primary-700",
|
||||
"focus-visible:before:tw-ring-primary-700",
|
||||
"disabled:hover:tw-border-primary-500",
|
||||
"disabled:hover:tw-bg-primary-500",
|
||||
],
|
||||
secondary: [
|
||||
@@ -46,6 +52,7 @@ const styles: Record<IconButtonStyle, string[]> = {
|
||||
"hover:!tw-text-contrast",
|
||||
"hover:tw-bg-text-muted",
|
||||
"focus-visible:before:tw-ring-primary-700",
|
||||
"disabled:hover:tw-border-text-muted",
|
||||
"disabled:hover:tw-bg-transparent",
|
||||
"disabled:hover:!tw-text-muted",
|
||||
"disabled:hover:tw-border-text-muted",
|
||||
@@ -57,6 +64,7 @@ const styles: Record<IconButtonStyle, string[]> = {
|
||||
"hover:!tw-text-contrast",
|
||||
"hover:tw-bg-danger-500",
|
||||
"focus-visible:before:tw-ring-primary-700",
|
||||
"disabled:hover:tw-border-danger-500",
|
||||
"disabled:hover:tw-bg-transparent",
|
||||
"disabled:hover:!tw-text-danger",
|
||||
"disabled:hover:tw-border-danger-500",
|
||||
@@ -72,12 +80,13 @@ const sizes: Record<IconButtonSize, string[]> = {
|
||||
|
||||
@Component({
|
||||
selector: "button[bitIconButton]",
|
||||
template: `<i class="bwi" [ngClass]="iconClass" aria-hidden="true"></i>`,
|
||||
templateUrl: "icon-button.component.html",
|
||||
providers: [{ provide: ButtonLikeAbstraction, useExisting: BitIconButtonComponent }],
|
||||
})
|
||||
export class BitIconButtonComponent {
|
||||
export class BitIconButtonComponent implements ButtonLikeAbstraction {
|
||||
@Input("bitIconButton") icon: string;
|
||||
|
||||
@Input() buttonType: IconButtonStyle = "main";
|
||||
@Input() buttonType: IconButtonType = "main";
|
||||
|
||||
@Input() size: IconButtonSize = "default";
|
||||
|
||||
@@ -90,7 +99,6 @@ export class BitIconButtonComponent {
|
||||
"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:
|
||||
@@ -117,4 +125,13 @@ export class BitIconButtonComponent {
|
||||
get iconClass() {
|
||||
return [this.icon, "!tw-m-0"];
|
||||
}
|
||||
|
||||
@HostBinding("attr.disabled")
|
||||
get disabledAttr() {
|
||||
const disabled = this.disabled != null && this.disabled !== false;
|
||||
return disabled || this.loading ? true : null;
|
||||
}
|
||||
|
||||
@Input() loading = false;
|
||||
@Input() disabled = false;
|
||||
}
|
||||
|
||||
@@ -1,59 +1,91 @@
|
||||
import { Meta, Story } from "@storybook/angular";
|
||||
|
||||
import { BitIconButtonComponent } from "./icon-button.component";
|
||||
import { BitIconButtonComponent, IconButtonType } from "./icon-button.component";
|
||||
|
||||
const buttonTypes: IconButtonType[] = [
|
||||
"contrast",
|
||||
"main",
|
||||
"muted",
|
||||
"primary",
|
||||
"secondary",
|
||||
"danger",
|
||||
];
|
||||
|
||||
export default {
|
||||
title: "Component Library/Icon Button",
|
||||
component: BitIconButtonComponent,
|
||||
args: {
|
||||
bitIconButton: "bwi-plus",
|
||||
buttonType: "primary",
|
||||
size: "default",
|
||||
disabled: false,
|
||||
},
|
||||
argTypes: {
|
||||
buttonTypes: { table: { disable: true } },
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
const Template: Story<BitIconButtonComponent> = (args: BitIconButtonComponent) => ({
|
||||
props: args,
|
||||
props: { ...args, buttonTypes },
|
||||
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>
|
||||
<table class="tw-border-spacing-2 tw-text-center tw-text-main">
|
||||
<thead>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td *ngFor="let buttonType of buttonTypes" class="tw-capitalize tw-font-bold tw-p-4"
|
||||
[class.tw-text-contrast]="buttonType === 'contrast'"
|
||||
[class.tw-bg-primary-500]="buttonType === 'contrast'">{{buttonType}}</td>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="tw-font-bold tw-p-4 tw-text-left">Default</td>
|
||||
<td *ngFor="let buttonType of buttonTypes" class="tw-p-2" [class.tw-bg-primary-500]="buttonType === 'contrast'">
|
||||
<button
|
||||
[bitIconButton]="bitIconButton"
|
||||
[buttonType]="buttonType"
|
||||
[size]="size"
|
||||
title="Example icon button"
|
||||
aria-label="Example icon button"></button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class="tw-font-bold tw-p-4 tw-text-left">Disabled</td>
|
||||
<td *ngFor="let buttonType of buttonTypes" class="tw-p-2" [class.tw-bg-primary-500]="buttonType === 'contrast'">
|
||||
<button
|
||||
[bitIconButton]="bitIconButton"
|
||||
[buttonType]="buttonType"
|
||||
[size]="size"
|
||||
disabled
|
||||
title="Example icon button"
|
||||
aria-label="Example icon button"></button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class="tw-font-bold tw-p-4 tw-text-left">Loading</td>
|
||||
<td *ngFor="let buttonType of buttonTypes" class="tw-p-2" [class.tw-bg-primary-500]="buttonType === 'contrast'">
|
||||
<button
|
||||
[bitIconButton]="bitIconButton"
|
||||
[buttonType]="buttonType"
|
||||
[size]="size"
|
||||
loading="true"
|
||||
title="Example icon button"
|
||||
aria-label="Example icon button"></button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
`,
|
||||
});
|
||||
|
||||
export const Contrast = Template.bind({});
|
||||
Contrast.args = {
|
||||
buttonType: "contrast",
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
size: "default",
|
||||
};
|
||||
|
||||
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",
|
||||
export const Small = Template.bind({});
|
||||
Small.args = {
|
||||
size: "small",
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user