Skip to content

Commit

Permalink
use rxjs instead of setTimeout, and separate loadingSpinner from loading
Browse files Browse the repository at this point in the history
vleague2 committed Jan 15, 2025
1 parent 2f8a4ae commit c207adf
Showing 6 changed files with 41 additions and 32 deletions.
61 changes: 33 additions & 28 deletions libs/components/src/async-actions/bit-action.directive.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Directive, HostListener, Input, OnDestroy, Optional } from "@angular/core";
import { BehaviorSubject, finalize, Subject, takeUntil, tap } from "rxjs";
import { BehaviorSubject, debounce, finalize, interval, Subject, takeUntil, tap } from "rxjs";

import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
@@ -21,44 +21,49 @@ 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,
) {}

private loadingDelay: NodeJS.Timeout | undefined = undefined;

get loading() {
return this._loading$.value;
}

set loading(value: boolean) {
if (value) {
this.loadingDelay = setTimeout(() => {
this.updateLoadingState(value);
}, 75);
} else {
if (this.loadingDelay !== undefined) {
clearTimeout(this.loadingDelay);
this.loadingDelay = undefined;
}

this.updateLoadingState(value);
}
}

private updateLoadingState(value: boolean) {
this._loading$.next(value);
this.buttonComponent.loading = value;
}

/**
* Determine whether it is appropriate to display a loading spinner. We only want to show
* a spinner if it's been more than 75 ms since the `loading` state began. This prevents
* a spinner "flash" for actions that are synchronous/nearly synchronous.
*
* We can't use `loading` for this, because we still need to disable the button during
* the full `loading` state. I.e. we only want the spinner to be debounced, not the
* loading/disabled state.
*/
private showLoadingSpinner$ = this._loading$.pipe(
debounce((isLoading) => interval(isLoading ? 75 : 0)),
);

disabled = false;

@Input("bitAction") handler: FunctionReturningAwaitable;

constructor(
private buttonComponent: ButtonLikeAbstraction,
@Optional() private validationService?: ValidationService,
@Optional() private logService?: LogService,
) {
this.showLoadingSpinner$.subscribe((showLoadingSpinner) => {
this.buttonComponent.showLoadingSpinner = showLoadingSpinner;
});
}

@HostListener("click")
protected async onClick() {
if (!this.handler || this.loading || this.disabled || this.buttonComponent.disabled) {
4 changes: 2 additions & 2 deletions libs/components/src/button/button.component.html
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
<span class="tw-relative">
<span [ngClass]="{ 'tw-invisible': loading }">
<span [ngClass]="{ 'tw-invisible': showLoadingSpinner }">
<ng-content></ng-content>
</span>
<span
class="tw-absolute tw-inset-0 tw-flex tw-items-center tw-justify-center"
[ngClass]="{ 'tw-invisible': !loading }"
[ngClass]="{ 'tw-invisible': !showLoadingSpinner }"
>
<i class="bwi bwi-spinner bwi-lg bwi-spin" aria-hidden="true"></i>
</span>
2 changes: 2 additions & 0 deletions libs/components/src/button/button.component.ts
Original file line number Diff line number Diff line change
@@ -98,5 +98,7 @@ export class ButtonComponent implements ButtonLikeAbstraction {

@Input() loading = false;

@Input() showLoadingSpinner = false;

@Input() disabled = false;
}
4 changes: 2 additions & 2 deletions libs/components/src/icon-button/icon-button.component.html
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
<span class="tw-relative">
<span [ngClass]="{ 'tw-invisible': loading }">
<span [ngClass]="{ 'tw-invisible': showLoadingSpinner }">
<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 }"
[ngClass]="{ 'tw-invisible': !showLoadingSpinner }"
>
<i
class="bwi bwi-spinner bwi-spin"
1 change: 1 addition & 0 deletions libs/components/src/icon-button/icon-button.component.ts
Original file line number Diff line number Diff line change
@@ -170,6 +170,7 @@ export class BitIconButtonComponent implements ButtonLikeAbstraction, FocusableE
}

@Input() loading = false;
@Input() showLoadingSpinner = false;
@Input() disabled = false;

getFocusTarget() {
1 change: 1 addition & 0 deletions libs/components/src/shared/button-like.abstraction.ts
Original file line number Diff line number Diff line change
@@ -5,4 +5,5 @@ export type ButtonType = "primary" | "secondary" | "danger" | "unstyled";
export abstract class ButtonLikeAbstraction {
loading: boolean;
disabled: boolean;
showLoadingSpinner: boolean;
}

0 comments on commit c207adf

Please sign in to comment.