Prevent the status indicator flickering for quick-returning commands (#265)

* add status-indicator-visible

* save

* save

* Prevent the status indicator flickering for quick returns

* flx regression

* reduce delay, reset spinnerVisible when there's no more running commands

* clean up code reuse

* move code around

* slight optimizations to prevent rendering before spinner is visible

* rename var

* revert shouldSync change as it broke the sync
This commit is contained in:
Evan Simkowitz 2024-01-30 16:31:38 -08:00 committed by GitHub
parent e576f7f07d
commit 40757fa7f4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 89 additions and 10 deletions

View File

@ -48,13 +48,33 @@
} }
} }
/*
The following accounts for a debounce in the status indicator. We will only display the status indicator icon if the parent indicates that it should be visible AND one of the following is true:
1. There is a status to be shown.
2. There is a spinner to be shown and the required delay has passed.
*/
.status-indicator-visible {
&.spinner-visible,
&.output,
&.error,
&.success {
.positional-icon-visible;
}
// This is set by the timeout in the status indicator component.
&.spinner-visible {
#spinner {
visibility: visible;
}
}
}
.status-indicator { .status-indicator {
#spinner, #spinner,
#indicator { #indicator {
visibility: hidden; visibility: hidden;
} }
.spin #spinner { .spin #spinner {
visibility: visible;
stroke: @term-white; stroke: @term-white;
} }
&.error #indicator { &.error #indicator {

View File

@ -3,6 +3,8 @@ import { StatusIndicatorLevel } from "../../../types/types";
import cn from "classnames"; import cn from "classnames";
import { ReactComponent as SpinnerIndicator } from "../../assets/icons/spinner-indicator.svg"; import { ReactComponent as SpinnerIndicator } from "../../assets/icons/spinner-indicator.svg";
import { boundMethod } from "autobind-decorator"; import { boundMethod } from "autobind-decorator";
import * as mobx from "mobx";
import * as mobxReact from "mobx-react";
import { ReactComponent as RotateIconSvg } from "../../assets/icons/line/rotate.svg"; import { ReactComponent as RotateIconSvg } from "../../assets/icons/line/rotate.svg";
@ -126,33 +128,90 @@ class SyncSpin extends React.Component<{
} }
interface StatusIndicatorProps { interface StatusIndicatorProps {
/**
* The level of the status indicator. This will determine the color of the status indicator.
*/
level: StatusIndicatorLevel; level: StatusIndicatorLevel;
className?: string; className?: string;
/**
* If true, a spinner will be shown around the status indicator.
*/
runningCommands?: boolean; runningCommands?: boolean;
} }
/**
* This component is used to show the status of a command. It will show a spinner around the status indicator if there are running commands. It will also delay showing the spinner for a short time to prevent flickering.
*/
@mobxReact.observer
export class StatusIndicator extends React.Component<StatusIndicatorProps> { export class StatusIndicator extends React.Component<StatusIndicatorProps> {
iconRef: React.RefObject<HTMLDivElement> = React.createRef(); iconRef: React.RefObject<HTMLDivElement> = React.createRef();
spinnerVisible: mobx.IObservableValue<boolean> = mobx.observable.box(false);
timeout: NodeJS.Timeout;
clearSpinnerTimeout() {
if (this.timeout) {
clearTimeout(this.timeout);
this.timeout = null;
}
mobx.action(() => {
this.spinnerVisible.set(false);
})();
}
/**
* This will apply a delay after there is a running command before showing the spinner. This prevents flickering for commands that return quickly.
*/
updateMountCallback() {
const runningCommands = this.props.runningCommands ?? false;
if (runningCommands && !this.timeout) {
this.timeout = setTimeout(
mobx.action(() => {
this.spinnerVisible.set(true);
}),
100
);
} else if (!runningCommands) {
this.clearSpinnerTimeout();
}
}
componentDidUpdate(): void {
this.updateMountCallback();
}
componentDidMount(): void {
this.updateMountCallback();
}
componentWillUnmount(): void {
this.clearSpinnerTimeout();
}
render() { render() {
const { level, className, runningCommands } = this.props; const { level, className, runningCommands } = this.props;
const spinnerVisible = this.spinnerVisible.get();
let statusIndicator = null; let statusIndicator = null;
if (level != StatusIndicatorLevel.None || runningCommands) { if (level != StatusIndicatorLevel.None || spinnerVisible) {
let levelClass = null; let indicatorLevelClass = null;
switch (level) { switch (level) {
case StatusIndicatorLevel.Output: case StatusIndicatorLevel.Output:
levelClass = "output"; indicatorLevelClass = "output";
break; break;
case StatusIndicatorLevel.Success: case StatusIndicatorLevel.Success:
levelClass = "success"; indicatorLevelClass = "success";
break; break;
case StatusIndicatorLevel.Error: case StatusIndicatorLevel.Error:
levelClass = "error"; indicatorLevelClass = "error";
break; break;
} }
const spinnerVisibleClass = spinnerVisible ? "spinner-visible" : null;
statusIndicator = ( statusIndicator = (
<CenteredIcon divRef={this.iconRef} className={cn(className, levelClass, "status-indicator")}> <CenteredIcon
<SpinnerIndicator className={runningCommands ? "spin" : null} /> divRef={this.iconRef}
className={cn(className, indicatorLevelClass, spinnerVisibleClass, "status-indicator")}
>
<SpinnerIndicator className={spinnerVisible ? "spin" : null} />
</CenteredIcon> </CenteredIcon>
); );
} }

View File

@ -188,7 +188,7 @@
} }
&:not(:hover) .status-indicator { &:not(:hover) .status-indicator {
.positional-icon-visible; .status-indicator-visible;
} }
&.workspaces { &.workspaces {

View File

@ -287,7 +287,7 @@
} }
&:not(:hover) .status-indicator { &:not(:hover) .status-indicator {
.positional-icon-visible; .status-indicator-visible;
} }
&:hover { &:hover {