Skip to content

Commit fc563b5

Browse files
committed
fontend: Loader: Fix to not freeze when main thread freezes
This avoids SVG that mui CircleProgress uses, so it doesn't freeze. Also avoids flashing if the loading is finished before 0.25 seconds. Fixes a11y issue to not use progressbar, because we always do not know the progress, but instead are announcing a status. Removes test which checks that people can add other props to it, because no one is using this functionality. Color and size props are used, and are still supported.
1 parent 1e5ee48 commit fc563b5

13 files changed

+219
-188
lines changed

frontend/src/components/authchooser/__snapshots__/AuthChooser.Testing.stories.storyshot

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -97,24 +97,17 @@
9797
class="MuiBox-root css-5cned0"
9898
>
9999
<span
100-
class="MuiCircularProgress-root MuiCircularProgress-indeterminate MuiCircularProgress-colorPrimary css-1g0vz9s-MuiCircularProgress-root"
101-
role="progressbar"
100+
aria-label="Testing auth"
101+
aria-live="polite"
102+
class="MuiCircularProgress-root MuiCircularProgress-indeterminate hl-custom-loader MuiCircularProgress-colorPrimary"
103+
role="status"
102104
style="width: 40px; height: 40px;"
103105
title="Testing auth"
104106
>
105-
<svg
106-
class="MuiCircularProgress-svg css-1idz92c-MuiCircularProgress-svg"
107-
viewBox="22 22 44 44"
108-
>
109-
<circle
110-
class="MuiCircularProgress-circle MuiCircularProgress-circleIndeterminate css-176wh8e-MuiCircularProgress-circle"
111-
cx="44"
112-
cy="44"
113-
fill="none"
114-
r="20.2"
115-
stroke-width="3.6"
116-
/>
117-
</svg>
107+
<span
108+
class="hl-custom-loader-circle"
109+
style="width: 100%; height: 100%; border-width: 4px; border-style: solid; border-color: #414141 transparent transparent transparent;"
110+
/>
118111
</span>
119112
</div>
120113
</main>

frontend/src/components/cluster/Charts/__snapshots__/StatusCharts.NodesStatusLoading.stories.storyshot

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -34,24 +34,17 @@
3434
class="MuiBox-root css-5cned0"
3535
>
3636
<span
37-
class="MuiCircularProgress-root MuiCircularProgress-indeterminate MuiCircularProgress-colorPrimary css-1g0vz9s-MuiCircularProgress-root"
38-
role="progressbar"
37+
aria-label="Loading data for "
38+
aria-live="polite"
39+
class="MuiCircularProgress-root MuiCircularProgress-indeterminate hl-custom-loader MuiCircularProgress-colorPrimary"
40+
role="status"
3941
style="width: 40px; height: 40px;"
4042
title="Loading data for "
4143
>
42-
<svg
43-
class="MuiCircularProgress-svg css-1idz92c-MuiCircularProgress-svg"
44-
viewBox="22 22 44 44"
45-
>
46-
<circle
47-
class="MuiCircularProgress-circle MuiCircularProgress-circleIndeterminate css-176wh8e-MuiCircularProgress-circle"
48-
cx="44"
49-
cy="44"
50-
fill="none"
51-
r="20.2"
52-
stroke-width="3.6"
53-
/>
54-
</svg>
44+
<span
45+
class="hl-custom-loader-circle"
46+
style="width: 100%; height: 100%; border-width: 4px; border-style: solid; border-color: #414141 transparent transparent transparent;"
47+
/>
5548
</span>
5649
</div>
5750
</div>

frontend/src/components/cluster/Charts/__snapshots__/StatusCharts.PodsStatusLoading.stories.storyshot

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -34,24 +34,17 @@
3434
class="MuiBox-root css-5cned0"
3535
>
3636
<span
37-
class="MuiCircularProgress-root MuiCircularProgress-indeterminate MuiCircularProgress-colorPrimary css-1g0vz9s-MuiCircularProgress-root"
38-
role="progressbar"
37+
aria-label="Loading data for "
38+
aria-live="polite"
39+
class="MuiCircularProgress-root MuiCircularProgress-indeterminate hl-custom-loader MuiCircularProgress-colorPrimary"
40+
role="status"
3941
style="width: 40px; height: 40px;"
4042
title="Loading data for "
4143
>
42-
<svg
43-
class="MuiCircularProgress-svg css-1idz92c-MuiCircularProgress-svg"
44-
viewBox="22 22 44 44"
45-
>
46-
<circle
47-
class="MuiCircularProgress-circle MuiCircularProgress-circleIndeterminate css-176wh8e-MuiCircularProgress-circle"
48-
cx="44"
49-
cy="44"
50-
fill="none"
51-
r="20.2"
52-
stroke-width="3.6"
53-
/>
54-
</svg>
44+
<span
45+
class="hl-custom-loader-circle"
46+
style="width: 100%; height: 100%; border-width: 4px; border-style: solid; border-color: #414141 transparent transparent transparent;"
47+
/>
5548
</span>
5649
</div>
5750
</div>

frontend/src/components/common/Chart.stories/__snapshots__/PercentageCircle.NoData.stories.storyshot

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,24 +14,17 @@
1414
class="MuiBox-root css-5cned0"
1515
>
1616
<span
17-
class="MuiCircularProgress-root MuiCircularProgress-indeterminate MuiCircularProgress-colorPrimary css-1g0vz9s-MuiCircularProgress-root"
18-
role="progressbar"
17+
aria-label="Loading data for CPU usage"
18+
aria-live="polite"
19+
class="MuiCircularProgress-root MuiCircularProgress-indeterminate hl-custom-loader MuiCircularProgress-colorPrimary"
20+
role="status"
1921
style="width: 40px; height: 40px;"
2022
title="Loading data for CPU usage"
2123
>
22-
<svg
23-
class="MuiCircularProgress-svg css-1idz92c-MuiCircularProgress-svg"
24-
viewBox="22 22 44 44"
25-
>
26-
<circle
27-
class="MuiCircularProgress-circle MuiCircularProgress-circleIndeterminate css-176wh8e-MuiCircularProgress-circle"
28-
cx="44"
29-
cy="44"
30-
fill="none"
31-
r="20.2"
32-
stroke-width="3.6"
33-
/>
34-
</svg>
24+
<span
25+
class="hl-custom-loader-circle"
26+
style="width: 100%; height: 100%; border-width: 4px; border-style: solid; border-color: #414141 transparent transparent transparent;"
27+
/>
3528
</span>
3629
</div>
3730
</div>

frontend/src/components/common/Loader.test.tsx

Lines changed: 6 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,11 @@ describe('Loader Component', () => {
2828
);
2929

3030
// Check if the container Box is present
31-
const container = screen.getByRole('progressbar').parentElement;
31+
const container = screen.getByRole('status').parentElement;
3232
expect(container).toHaveClass('MuiBox-root');
3333

3434
// Check if CircularProgress is rendered
35-
const progress = screen.getByRole('progressbar');
35+
const progress = screen.getByRole('status');
3636
expect(progress).toHaveClass('MuiCircularProgress-root');
3737
expect(progress).toHaveAttribute('title', 'Loading...');
3838
});
@@ -45,7 +45,7 @@ describe('Loader Component', () => {
4545
);
4646

4747
// Check if CircularProgress is rendered directly without container
48-
const progress = screen.getByRole('progressbar');
48+
const progress = screen.getByRole('status');
4949
expect(progress).toHaveClass('MuiCircularProgress-root');
5050
expect(progress.parentElement).not.toHaveClass('MuiBox-root');
5151
});
@@ -58,7 +58,7 @@ describe('Loader Component', () => {
5858
</TestContext>
5959
);
6060

61-
const progress = screen.getByRole('progressbar');
61+
const progress = screen.getByRole('status');
6262
expect(progress).toHaveStyle({ width: `${customSize}px`, height: `${customSize}px` });
6363
});
6464

@@ -69,7 +69,7 @@ describe('Loader Component', () => {
6969
</TestContext>
7070
);
7171

72-
const progress = screen.getByRole('progressbar');
72+
const progress = screen.getByRole('status');
7373
expect(progress).toHaveClass('MuiCircularProgress-colorSecondary');
7474
});
7575

@@ -80,20 +80,7 @@ describe('Loader Component', () => {
8080
</TestContext>
8181
);
8282

83-
const progress = screen.getByRole('progressbar');
83+
const progress = screen.getByRole('status');
8484
expect(progress).toHaveAttribute('title', '');
8585
});
86-
87-
it('passes additional props to CircularProgress', () => {
88-
render(
89-
<TestContext>
90-
<Loader title="Loading..." thickness={4} disableShrink />
91-
</TestContext>
92-
);
93-
94-
const progress = screen.getByRole('progressbar');
95-
expect(progress).toHaveClass('MuiCircularProgress-root');
96-
expect(progress).toHaveAttribute('role', 'progressbar');
97-
expect(progress).toHaveAttribute('title', 'Loading...');
98-
});
9986
});

frontend/src/components/common/Loader.tsx

Lines changed: 125 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,138 @@
1515
*/
1616

1717
import Box from '@mui/material/Box';
18-
import CircularProgress, { CircularProgressProps } from '@mui/material/CircularProgress';
18+
import { useTheme } from '@mui/material/styles';
1919
import React from 'react';
2020

21-
export interface LoaderProps extends CircularProgressProps {
21+
/**
22+
* - Uses CSS animations so does not freeze when main thread is busy
23+
* - Does not display right away, to avoid flash of spinner
24+
*/
25+
26+
export interface LoaderProps {
27+
/** If true, the loader will not be wrapped in a container Box. */
2228
noContainer?: boolean;
29+
/** Title for the loader, used for accessibility and as a tooltip. */
2330
title: string;
31+
/** Size of the loader. Defaults to 40. */
32+
size?: number;
33+
/** Color of the loader. Defaults to primary color. */
34+
color?: 'primary' | 'secondary' | 'error' | 'info' | 'success' | 'warning' | string;
35+
}
36+
37+
/**
38+
* Injects custom loader styles into the document, once for each theme.
39+
*/
40+
function injectLoaderStyle(theme: any) {
41+
const styleId = `hl-custom-loader-style-${theme.palette.primary.main.replace(
42+
/[^a-zA-Z0-9]/g,
43+
''
44+
)}`;
45+
if (!document.getElementById(styleId)) {
46+
const style = document.createElement('style');
47+
style.id = styleId;
48+
style.textContent = `
49+
@keyframes hl-custom-loader-spin {
50+
100% { transform: rotate(360deg); }
51+
}
52+
@keyframes hl-custom-loader-delay {
53+
0% { opacity: 0; }
54+
99% { opacity: 0; }
55+
100% { opacity: 1; }
56+
}
57+
.hl-custom-loader {
58+
display: inline-block;
59+
position: relative;
60+
opacity: 0;
61+
animation: hl-custom-loader-delay 0.25s linear forwards;
62+
}
63+
.hl-custom-loader-circle {
64+
box-sizing: border-box;
65+
position: absolute;
66+
top: 0; left: 0;
67+
border-radius: 50%;
68+
border-right-color: transparent;
69+
border-bottom-color: transparent;
70+
animation: hl-custom-loader-spin 0.8s linear infinite;
71+
}
72+
`;
73+
document.head.appendChild(style);
74+
}
75+
}
76+
77+
/**
78+
* @returns a color value based on the theme and the provided color prop.
79+
* If no color is provided, it defaults to the primary color of the theme.
80+
*/
81+
function getColor(theme: any, color?: LoaderProps['color']) {
82+
if (!color || color === 'primary') return theme.palette.primary.main;
83+
if (color === 'secondary') return theme.palette.secondary.main;
84+
if (color === 'error') return theme.palette.error.main;
85+
if (color === 'info') return theme.palette.info.main;
86+
if (color === 'success') return theme.palette.success.main;
87+
if (color === 'warning') return theme.palette.warning.main;
88+
return color;
89+
}
90+
91+
/**
92+
* @returns a MUI class name based on the provided color prop.
93+
* This is used to apply the correct color to the CircularProgress component.
94+
*/
95+
function getMuiColorClass(color?: LoaderProps['color']) {
96+
if (!color || color === 'primary') return 'MuiCircularProgress-colorPrimary';
97+
if (color === 'secondary') return 'MuiCircularProgress-colorSecondary';
98+
if (color === 'error') return 'MuiCircularProgress-colorError';
99+
if (color === 'info') return 'MuiCircularProgress-colorInfo';
100+
if (color === 'success') return 'MuiCircularProgress-colorSuccess';
101+
if (color === 'warning') return 'MuiCircularProgress-colorWarning';
102+
return '';
103+
}
104+
105+
/**
106+
* CustomLoader component that renders a circular loader with custom styles.
107+
*/
108+
function CustomLoader({
109+
title,
110+
size = 40,
111+
color,
112+
}: {
113+
title: LoaderProps['title'];
114+
size?: LoaderProps['size'];
115+
color?: LoaderProps['color'];
116+
}) {
117+
const theme = useTheme();
118+
React.useEffect(() => {
119+
injectLoaderStyle(theme);
120+
}, [theme]);
121+
const loaderColor = getColor(theme, color);
122+
const muiColorClass = getMuiColorClass(color);
123+
124+
return (
125+
<span
126+
className={`MuiCircularProgress-root MuiCircularProgress-indeterminate hl-custom-loader ${muiColorClass}`}
127+
role="status"
128+
aria-label={title}
129+
aria-live="polite"
130+
title={title}
131+
style={{ width: size, height: size }}
132+
>
133+
<span
134+
className="hl-custom-loader-circle"
135+
style={{
136+
width: '100%',
137+
height: '100%',
138+
borderWidth: Math.max(2, Math.round(size / 10)),
139+
borderStyle: 'solid',
140+
borderColor: `${loaderColor} transparent transparent transparent`,
141+
}}
142+
/>
143+
</span>
144+
);
24145
}
25146

26147
export default function Loader(props: LoaderProps) {
27-
const { noContainer = false, title, ...other } = props;
28-
const progress = <CircularProgress title={title} {...other} />;
148+
const { noContainer = false, title, size, color } = props;
149+
const progress = <CustomLoader title={title} size={size} color={color} />;
29150

30151
if (noContainer) return progress;
31152

frontend/src/components/common/__snapshots__/Loader.CustomColor.stories.storyshot

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,17 @@
44
class="MuiBox-root css-5cned0"
55
>
66
<span
7-
class="MuiCircularProgress-root MuiCircularProgress-indeterminate MuiCircularProgress-colorSecondary css-q10m9g-MuiCircularProgress-root"
8-
role="progressbar"
7+
aria-label="Loading with custom color..."
8+
aria-live="polite"
9+
class="MuiCircularProgress-root MuiCircularProgress-indeterminate hl-custom-loader MuiCircularProgress-colorSecondary"
10+
role="status"
911
style="width: 40px; height: 40px;"
1012
title="Loading with custom color..."
1113
>
12-
<svg
13-
class="MuiCircularProgress-svg css-1idz92c-MuiCircularProgress-svg"
14-
viewBox="22 22 44 44"
15-
>
16-
<circle
17-
class="MuiCircularProgress-circle MuiCircularProgress-circleIndeterminate css-176wh8e-MuiCircularProgress-circle"
18-
cx="44"
19-
cy="44"
20-
fill="none"
21-
r="20.2"
22-
stroke-width="3.6"
23-
/>
24-
</svg>
14+
<span
15+
class="hl-custom-loader-circle"
16+
style="width: 100%; height: 100%; border-width: 4px; border-style: solid; border-color: #eff2f5 transparent transparent transparent;"
17+
/>
2518
</span>
2619
</div>
2720
</div>

0 commit comments

Comments
 (0)