1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-13 23:03:32 +00:00

[CL-485] Add small delay for async action loading state (#12835)

This commit is contained in:
Vicki League
2025-02-25 09:56:01 -05:00
committed by GitHub
parent d11321e28e
commit 6d1914f43d
16 changed files with 253 additions and 97 deletions

View File

@@ -21,30 +21,35 @@ export class BitActionDirective implements OnDestroy {
private destroy$ = new Subject<void>();
private _loading$ = new BehaviorSubject<boolean>(false);
disabled = false;
@Input("bitAction") handler: FunctionReturningAwaitable;
/**
* Observable of loading behavior subject
*
* Used in `form-button.directive.ts`
*/
readonly loading$ = this._loading$.asObservable();
constructor(
private buttonComponent: ButtonLikeAbstraction,
@Optional() private validationService?: ValidationService,
@Optional() private logService?: LogService,
) {}
get loading() {
return this._loading$.value;
}
set loading(value: boolean) {
this._loading$.next(value);
this.buttonComponent.loading = value;
this.buttonComponent.loading.set(value);
}
disabled = false;
@Input("bitAction") handler: FunctionReturningAwaitable;
constructor(
private buttonComponent: ButtonLikeAbstraction,
@Optional() private validationService?: ValidationService,
@Optional() private logService?: LogService,
) {}
@HostListener("click")
protected async onClick() {
if (!this.handler || this.loading || this.disabled || this.buttonComponent.disabled) {
if (!this.handler || this.loading || this.disabled || this.buttonComponent.disabled()) {
return;
}

View File

@@ -41,15 +41,15 @@ export class BitFormButtonDirective implements OnDestroy {
if (submitDirective && buttonComponent) {
submitDirective.loading$.pipe(takeUntil(this.destroy$)).subscribe((loading) => {
if (this.type === "submit") {
buttonComponent.loading = loading;
buttonComponent.loading.set(loading);
} else {
buttonComponent.disabled = this.disabled || loading;
buttonComponent.disabled.set(this.disabled || loading);
}
});
submitDirective.disabled$.pipe(takeUntil(this.destroy$)).subscribe((disabled) => {
if (this.disabled !== false) {
buttonComponent.disabled = this.disabled || disabled;
buttonComponent.disabled.set(this.disabled || disabled);
}
});
}

View File

@@ -1,6 +1,7 @@
import { Meta } from "@storybook/addon-docs";
import { Meta, Story } from "@storybook/addon-docs";
import * as stories from "./standalone.stories.ts";
<Meta title="Component Library/Async Actions/Standalone/Documentation" />
<Meta of={stories} />
# Standalone Async Actions
@@ -8,9 +9,13 @@ These directives should be used when building a standalone button that triggers
in the background, eg. Refresh buttons. For non-submit buttons that are associated with forms see
[Async Actions In Forms](?path=/story/component-library-async-actions-in-forms-documentation--page).
If the long running background task resolves quickly (e.g. less than 75 ms), the loading spinner
will not display on the button. This prevents an undesirable "flicker" of the loading spinner when
it is not necessary for the user to see it.
## Usage
Adding async actions to standalone buttons requires the following 2 steps
Adding async actions to standalone buttons requires the following 2 steps:
### 1. Add a handler to your `Component`
@@ -60,3 +65,21 @@ from how click handlers are usually defined with the output syntax `(click)="han
<button bitIconButton="bwi-trash" [bitAction]="handler"></button>`;
```
## Stories
### Promise resolves -- loading spinner is displayed
<Story of={stories.UsingPromise} />
### Promise resolves -- quickly without loading spinner
<Story of={stories.ActionResolvesQuickly} />
### Promise rejects
<Story of={stories.RejectedPromise} />
### Observable
<Story of={stories.UsingObservable} />

View File

@@ -11,9 +11,9 @@ import { IconButtonModule } from "../icon-button";
import { BitActionDirective } from "./bit-action.directive";
const template = `
const template = /*html*/ `
<button bitButton buttonType="primary" [bitAction]="action" class="tw-mr-2">
Perform action
Perform action {{ statusEmoji }}
</button>
<button bitIconButton="bwi-trash" buttonType="danger" [bitAction]="action"></button>`;
@@ -22,9 +22,30 @@ const template = `
selector: "app-promise-example",
})
class PromiseExampleComponent {
statusEmoji = "🟡";
action = async () => {
await new Promise<void>((resolve, reject) => {
setTimeout(resolve, 2000);
setTimeout(() => {
resolve();
this.statusEmoji = "🟢";
}, 5000);
});
};
}
@Component({
template,
selector: "app-action-resolves-quickly",
})
class ActionResolvesQuicklyComponent {
statusEmoji = "🟡";
action = async () => {
await new Promise<void>((resolve, reject) => {
setTimeout(() => {
resolve();
this.statusEmoji = "🟢";
}, 50);
});
};
}
@@ -59,6 +80,7 @@ export default {
PromiseExampleComponent,
ObservableExampleComponent,
RejectedPromiseExampleComponent,
ActionResolvesQuicklyComponent,
],
imports: [ButtonModule, IconButtonModule, BitActionDirective],
providers: [
@@ -100,3 +122,10 @@ export const RejectedPromise: ObservableStory = {
template: `<app-rejected-promise-example></app-rejected-promise-example>`,
}),
};
export const ActionResolvesQuickly: PromiseStory = {
render: (args) => ({
props: args,
template: `<app-action-resolves-quickly></app-action-resolves-quickly>`,
}),
};