Skip to content

Commit 40757fa

Browse files
authored
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
1 parent e576f7f commit 40757fa

4 files changed

Lines changed: 89 additions & 10 deletions

File tree

src/app/common/icons/icons.less

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,33 @@
4848
}
4949
}
5050

51+
/*
52+
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:
53+
1. There is a status to be shown.
54+
2. There is a spinner to be shown and the required delay has passed.
55+
*/
56+
.status-indicator-visible {
57+
&.spinner-visible,
58+
&.output,
59+
&.error,
60+
&.success {
61+
.positional-icon-visible;
62+
}
63+
64+
// This is set by the timeout in the status indicator component.
65+
&.spinner-visible {
66+
#spinner {
67+
visibility: visible;
68+
}
69+
}
70+
}
71+
5172
.status-indicator {
5273
#spinner,
5374
#indicator {
5475
visibility: hidden;
5576
}
5677
.spin #spinner {
57-
visibility: visible;
5878
stroke: @term-white;
5979
}
6080
&.error #indicator {

src/app/common/icons/icons.tsx

Lines changed: 66 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { StatusIndicatorLevel } from "../../../types/types";
33
import cn from "classnames";
44
import { ReactComponent as SpinnerIndicator } from "../../assets/icons/spinner-indicator.svg";
55
import { boundMethod } from "autobind-decorator";
6+
import * as mobx from "mobx";
7+
import * as mobxReact from "mobx-react";
68

79
import { ReactComponent as RotateIconSvg } from "../../assets/icons/line/rotate.svg";
810

@@ -126,33 +128,90 @@ class SyncSpin extends React.Component<{
126128
}
127129

128130
interface StatusIndicatorProps {
131+
/**
132+
* The level of the status indicator. This will determine the color of the status indicator.
133+
*/
129134
level: StatusIndicatorLevel;
130135
className?: string;
136+
/**
137+
* If true, a spinner will be shown around the status indicator.
138+
*/
131139
runningCommands?: boolean;
132140
}
133141

142+
/**
143+
* 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.
144+
*/
145+
@mobxReact.observer
134146
export class StatusIndicator extends React.Component<StatusIndicatorProps> {
135147
iconRef: React.RefObject<HTMLDivElement> = React.createRef();
148+
spinnerVisible: mobx.IObservableValue<boolean> = mobx.observable.box(false);
149+
timeout: NodeJS.Timeout;
150+
151+
clearSpinnerTimeout() {
152+
if (this.timeout) {
153+
clearTimeout(this.timeout);
154+
this.timeout = null;
155+
}
156+
mobx.action(() => {
157+
this.spinnerVisible.set(false);
158+
})();
159+
}
160+
161+
/**
162+
* This will apply a delay after there is a running command before showing the spinner. This prevents flickering for commands that return quickly.
163+
*/
164+
updateMountCallback() {
165+
const runningCommands = this.props.runningCommands ?? false;
166+
if (runningCommands && !this.timeout) {
167+
this.timeout = setTimeout(
168+
mobx.action(() => {
169+
this.spinnerVisible.set(true);
170+
}),
171+
100
172+
);
173+
} else if (!runningCommands) {
174+
this.clearSpinnerTimeout();
175+
}
176+
}
177+
178+
componentDidUpdate(): void {
179+
this.updateMountCallback();
180+
}
181+
182+
componentDidMount(): void {
183+
this.updateMountCallback();
184+
}
185+
186+
componentWillUnmount(): void {
187+
this.clearSpinnerTimeout();
188+
}
136189

137190
render() {
138191
const { level, className, runningCommands } = this.props;
192+
const spinnerVisible = this.spinnerVisible.get();
139193
let statusIndicator = null;
140-
if (level != StatusIndicatorLevel.None || runningCommands) {
141-
let levelClass = null;
194+
if (level != StatusIndicatorLevel.None || spinnerVisible) {
195+
let indicatorLevelClass = null;
142196
switch (level) {
143197
case StatusIndicatorLevel.Output:
144-
levelClass = "output";
198+
indicatorLevelClass = "output";
145199
break;
146200
case StatusIndicatorLevel.Success:
147-
levelClass = "success";
201+
indicatorLevelClass = "success";
148202
break;
149203
case StatusIndicatorLevel.Error:
150-
levelClass = "error";
204+
indicatorLevelClass = "error";
151205
break;
152206
}
207+
208+
const spinnerVisibleClass = spinnerVisible ? "spinner-visible" : null;
153209
statusIndicator = (
154-
<CenteredIcon divRef={this.iconRef} className={cn(className, levelClass, "status-indicator")}>
155-
<SpinnerIndicator className={runningCommands ? "spin" : null} />
210+
<CenteredIcon
211+
divRef={this.iconRef}
212+
className={cn(className, indicatorLevelClass, spinnerVisibleClass, "status-indicator")}
213+
>
214+
<SpinnerIndicator className={spinnerVisible ? "spin" : null} />
156215
</CenteredIcon>
157216
);
158217
}

src/app/sidebar/sidebar.less

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@
188188
}
189189

190190
&:not(:hover) .status-indicator {
191-
.positional-icon-visible;
191+
.status-indicator-visible;
192192
}
193193

194194
&.workspaces {

src/app/workspace/screen/tabs.less

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,7 @@
287287
}
288288

289289
&:not(:hover) .status-indicator {
290-
.positional-icon-visible;
290+
.status-indicator-visible;
291291
}
292292

293293
&:hover {

0 commit comments

Comments
 (0)