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:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>`,
|
||||
}),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user