Skip to content

Commit 4b30486

Browse files
authored
ITEP-67176 merge loaders (#371)
1 parent 6a28633 commit 4b30486

File tree

47 files changed

+352
-216
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+352
-216
lines changed

web_ui/packages/ui/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,6 @@ export { Slider } from './src/slider/slider.component';
129129
export { Switch } from './src/switch/switch.component';
130130
export { SearchField } from './src/search-field/search-field.component';
131131
export { Loading } from './src/loading/loading.component';
132-
export { LoadingIndicator } from './src/loading/loading-indicator.component';
133132
export { IntelBrandedLoading } from './src/loading/intel-branded-loading.component';
134133
export { Breadcrumbs } from './src/breadcrumbs/breadcrumbs.component';
135134
export { type BreadcrumbsProps } from './src/breadcrumbs/breadcrumbs.interface';

web_ui/packages/ui/src/loading/loading-indicator.component.tsx

Lines changed: 0 additions & 19 deletions
This file was deleted.
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
// Copyright (C) 2022-2025 Intel Corporation
2+
// LIMITED EDGE SOFTWARE DISTRIBUTION LICENSE
3+
4+
import { ReactNode } from 'react';
5+
6+
import { darkTheme, Provider } from '@adobe/react-spectrum';
7+
import { render, screen } from '@testing-library/react';
8+
9+
import { Loading } from './loading.component';
10+
11+
const TestProvider = ({ children }: { children: ReactNode }) => {
12+
return <Provider theme={darkTheme}>{children}</Provider>;
13+
};
14+
15+
describe('Loading', () => {
16+
const renderLoading = (props = {}) => {
17+
return render(
18+
<TestProvider>
19+
<Loading {...props} />
20+
</TestProvider>
21+
);
22+
};
23+
24+
describe('inline mode', () => {
25+
it('renders inline loading spinner without overlay', () => {
26+
renderLoading({ mode: 'inline' });
27+
28+
const progressbar = screen.getByRole('progressbar');
29+
expect(progressbar).toBeInTheDocument();
30+
expect(progressbar).toHaveAttribute('aria-label', 'Loading...');
31+
32+
// In inline mode, there should be no overlay containers
33+
const positionContainer = progressbar.closest('[style*="position: absolute"]');
34+
const classContainer = progressbar.closest('[class*="overlay"]');
35+
expect(positionContainer).not.toBeInTheDocument();
36+
expect(classContainer).not.toBeInTheDocument();
37+
});
38+
39+
it('supports custom props and styling in inline mode', () => {
40+
renderLoading({
41+
mode: 'inline',
42+
size: 'S',
43+
style: {
44+
height: '150px',
45+
},
46+
'aria-label': 'Custom loading message',
47+
value: 50,
48+
});
49+
50+
const progressbar = screen.getByRole('progressbar');
51+
expect(progressbar).toHaveAttribute('aria-label', 'Custom loading message');
52+
expect(progressbar).toHaveAttribute('aria-valuemax', '100');
53+
54+
// Check spinner container height
55+
const flexContainer = progressbar.parentElement;
56+
expect(flexContainer).toHaveStyle({ height: '150px' });
57+
});
58+
});
59+
60+
describe('fullscreen mode', () => {
61+
it('renders with default styling and overlay', () => {
62+
renderLoading(); // Default mode is fullscreen
63+
64+
const progressbar = screen.getByRole('progressbar');
65+
expect(progressbar).toBeInTheDocument();
66+
expect(progressbar).toHaveAttribute('aria-label', 'Loading...');
67+
68+
// Should render overlay container with CSS classes and default background
69+
const container = progressbar.parentElement;
70+
expect(container).toBeInTheDocument();
71+
expect(container).toHaveClass('overlay', 'fullscreen');
72+
expect(container).toHaveStyle({ backgroundColor: 'var(--spectrum-global-color-gray-50)' });
73+
});
74+
75+
it('supports custom styling and spinner height', () => {
76+
renderLoading({
77+
mode: 'fullscreen',
78+
className: 'custom-loading-class',
79+
style: {
80+
backgroundColor: 'rgba(255, 0, 0, 0.5)',
81+
left: 10,
82+
right: 20,
83+
top: 30,
84+
bottom: 40,
85+
paddingTop: '2rem',
86+
marginTop: '1rem',
87+
height: '50vh',
88+
},
89+
});
90+
91+
const progressbar = screen.getByRole('progressbar');
92+
expect(progressbar).toBeInTheDocument();
93+
94+
const container = progressbar.parentElement;
95+
expect(container).toBeInTheDocument();
96+
expect(container).toHaveClass('custom-loading-class', 'overlay', 'fullscreen');
97+
expect(container).toHaveStyle({
98+
backgroundColor: 'rgba(255, 0, 0, 0.5)',
99+
left: '10px',
100+
right: '20px',
101+
top: '30px',
102+
bottom: '40px',
103+
paddingTop: '2rem',
104+
marginTop: '1rem',
105+
height: '50vh',
106+
});
107+
});
108+
});
109+
110+
describe('overlay mode', () => {
111+
it('renders with modal styling, custom props, and multiple style properties', () => {
112+
renderLoading({
113+
mode: 'overlay',
114+
style: {
115+
backgroundColor: 'rgba(0, 0, 0, 0.8)',
116+
height: '100vh',
117+
paddingTop: '50px',
118+
},
119+
className: 'test-class',
120+
});
121+
122+
const progressbar = screen.getByRole('progressbar');
123+
expect(progressbar).toBeInTheDocument();
124+
125+
const container = progressbar.parentElement;
126+
expect(container).toBeInTheDocument();
127+
expect(container).toHaveClass('test-class', 'overlay', 'modal');
128+
expect(container).toHaveStyle({
129+
backgroundColor: 'rgba(0, 0, 0, 0.8)',
130+
height: '100vh',
131+
paddingTop: '50px',
132+
});
133+
});
134+
});
135+
136+
describe('accessibility', () => {
137+
it('supports custom aria-label and is indeterminate by default', () => {
138+
renderLoading({ 'aria-label': 'Loading data...' });
139+
140+
const progressbar = screen.getByRole('progressbar');
141+
expect(progressbar).toHaveAttribute('aria-label', 'Loading data...');
142+
// ProgressCircle should be indeterminate (no value)
143+
expect(progressbar).not.toHaveAttribute('aria-valuenow');
144+
});
145+
});
146+
});
Lines changed: 83 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,96 @@
11
// Copyright (C) 2022-2025 Intel Corporation
22
// LIMITED EDGE SOFTWARE DISTRIBUTION LICENSE
33

4-
import { View } from '@adobe/react-spectrum';
5-
import { SpectrumProgressCircleProps } from '@react-types/progress';
4+
import { CSSProperties } from 'react';
65

7-
import { LoadingIndicator } from './loading-indicator.component';
6+
import { Flex, ProgressCircle, View } from '@adobe/react-spectrum';
7+
import { SpectrumProgressCircleProps } from '@react-types/progress';
8+
import { clsx } from 'clsx';
89

910
import classes from './loading.module.scss';
1011

11-
interface LoadingProps extends Partial<SpectrumProgressCircleProps> {
12-
overlay?: boolean;
13-
id?: string;
12+
/**
13+
* Props for the Loading component.
14+
*
15+
* Extends Adobe Spectrum's ProgressCircle props to provide a flexible loading indicator
16+
* suitable for various use cases.
17+
*/
18+
interface LoadingProps extends SpectrumProgressCircleProps {
19+
/**
20+
* The display mode for the loading component.
21+
* - 'inline': Renders as an inline element without overlay positioning
22+
* - 'fullscreen': Renders as a full-screen overlay with default background
23+
* - 'overlay': Renders as a positioned overlay with modal background
24+
* @default 'fullscreen'
25+
*/
26+
mode?: 'inline' | 'fullscreen' | 'overlay';
27+
28+
/**
29+
* CSS styles for the container.
30+
* For inline mode: styles the Flex container
31+
* For overlay modes: styles the overlay View container
32+
*/
33+
style?: CSSProperties;
34+
35+
className?: string;
1436
}
1537

16-
export const Loading = ({ overlay = false, id, ...rest }: LoadingProps): JSX.Element => {
38+
/**
39+
* A flexible loading component that can display a progress circle in different modes.
40+
*
41+
* This component consolidates various loading patterns used throughout the application:
42+
* - Inline loading indicators
43+
* - Full-screen loading overlays
44+
* - Modal-style loading overlays
45+
*
46+
* The component automatically handles positioning, background colors, and accessibility
47+
* attributes based on the selected mode.
48+
*
49+
* @example
50+
* ```tsx
51+
* // Simple inline loading spinner
52+
* <Loading mode="inline" size="M" />
53+
*
54+
* // Full-screen overlay with custom background
55+
* <Loading mode="overlay" style={{ backgroundColor: "rgba(0,0,0,0.5)" }} />
56+
*
57+
* // Custom positioned overlay
58+
* <Loading mode="overlay" style={{
59+
* top: 100,
60+
* left: 50,
61+
* width: 200,
62+
* height: 200,
63+
* backgroundColor: "white"
64+
* }} />
65+
*
66+
* // Conditional loading overlay
67+
* {isLoading && <Loading mode="overlay" />}
68+
* ```
69+
*
70+
* @param props - The component props
71+
* @returns A loading component with progress circle
72+
*/
73+
export const Loading = ({
74+
mode = 'fullscreen',
75+
size = 'L',
76+
style = {},
77+
className,
78+
...rest
79+
}: LoadingProps): JSX.Element => {
80+
if (mode === 'inline') {
81+
return (
82+
<Flex alignItems={'center'} justifyContent={'center'} UNSAFE_style={style} UNSAFE_className={className}>
83+
<ProgressCircle aria-label={'Loading...'} isIndeterminate size={size} {...rest} />
84+
</Flex>
85+
);
86+
}
87+
88+
// Determine CSS classes based on mode
89+
const overlayClassName = clsx(classes.overlay, mode === 'overlay' ? classes.modal : classes.fullscreen, className);
90+
1791
return (
18-
<View
19-
height={'100%'}
20-
position={'absolute'}
21-
left={0}
22-
top={0}
23-
right={0}
24-
bottom={0}
25-
id={id}
26-
data-testid={id}
27-
UNSAFE_className={overlay ? classes.loadingBackground : ''}
28-
>
29-
<LoadingIndicator {...rest} />
92+
<View UNSAFE_style={style} UNSAFE_className={overlayClassName}>
93+
<ProgressCircle aria-label={'Loading...'} isIndeterminate size={size} {...rest} />
3094
</View>
3195
);
3296
};
Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,22 @@
1-
.loadingBackground {
2-
background-color: var(--spectrum-alias-background-color-modal-overlay);
3-
z-index: 10;
1+
.overlay {
2+
position: absolute;
3+
left: 0;
4+
right: 0;
5+
top: 0;
6+
bottom: 0;
7+
height: 100%;
8+
cursor: default;
9+
display: flex;
10+
align-items: center;
11+
justify-content: center;
412
}
13+
14+
.fullscreen {
15+
background-color: var(--spectrum-global-color-gray-50);
16+
}
17+
18+
.modal {
19+
background-color: var(--spectrum-alias-background-color-modal-overlay);
20+
z-index: 20;
21+
}
22+

web_ui/packages/ui/src/virtualize-list-layout/virtualize-list-layout.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
Virtualizer,
1616
} from 'react-aria-components';
1717

18-
import { LoadingIndicator } from '../loading/loading-indicator.component';
18+
import { Loading } from '../loading/loading.component';
1919

2020
import classes from './virtualize-list-layout.module.scss';
2121

@@ -40,7 +40,7 @@ export const VirtualizedListLayout = <T,>({
4040
ariaLabel,
4141
layoutOptions,
4242
containerHeight,
43-
renderLoading = () => <LoadingIndicator size={'M'} />,
43+
renderLoading = () => <Loading mode='inline' size={'M'} />,
4444
renderItem,
4545
onLoadMore,
4646
idFormatter,

web_ui/src/pages/annotator/annotation/annotation-list/annotation-list.component.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export const AnnotationList = ({ annotations, isLoading, isDragDisabled }: Annot
6262
if (isLoading) {
6363
return (
6464
<Content position={'relative'} UNSAFE_style={{ height: '100%' }}>
65-
<Loading size='M' id={'annotation-list-loader'} />
65+
<Loading size='M' />
6666
</Content>
6767
);
6868
}

web_ui/src/pages/annotator/components/main-content/main-content.component.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,9 +109,7 @@ export const MainContent = ({ labels, annotationToolContext }: MainContentProps)
109109
</AnnotatorCanvasSettings>
110110
</TransformZoomAnnotation>
111111

112-
{(isLoading || isSaving) && (
113-
<Loading overlay data-testid={'main-content-loader-id'} id={'main-content-loader-id'} />
114-
)}
112+
{(isLoading || isSaving) && <Loading mode='overlay' />}
115113

116114
{annotationToolContext.tool === ToolType.SegmentAnythingTool && <SegmentAnythingLoader />}
117115

web_ui/src/pages/annotator/components/range-based-video-player/video-content.component.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export const VideoContent = ({ mediaItem, video, setFrameNumber, onPlay, onPause
4646
onSeeked={() => setIsVideoFrameLoading(false)}
4747
preload={'auto'}
4848
/>
49-
{showLoading && <Loading overlay />}
49+
{showLoading && <Loading mode='overlay' />}
5050
</View>
5151
</TransformZoom>
5252
</View>

web_ui/src/pages/annotator/components/range-based-video-player/video-player-dialog.component.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import { useRef, useState } from 'react';
55

6-
import { Button, ButtonGroup, Content, Dialog, Divider, Flex, Heading, LoadingIndicator, View } from '@geti/ui';
6+
import { Button, ButtonGroup, Content, Dialog, Divider, Flex, Heading, Loading, View } from '@geti/ui';
77

88
import { LabeledVideoRange } from '../../../../core/annotations/labeled-video-range.interface';
99
import { MEDIA_TYPE } from '../../../../core/media/base-media.interface';
@@ -124,7 +124,7 @@ export const VideoPlayerDialog = ({
124124

125125
{rangesQuery.isFetching ? (
126126
<Content UNSAFE_style={{ overflowY: 'visible' }} height='size-4600'>
127-
<LoadingIndicator />
127+
<Loading mode='inline' />
128128
</Content>
129129
) : (
130130
<Content UNSAFE_style={{ overflowY: 'visible' }}>
@@ -162,7 +162,7 @@ export const VideoPlayerDialog = ({
162162
Cancel
163163
</Button>
164164
<Button variant='accent' onPress={handleSaveSelection} isDisabled={!undoRedoState.canUndo}>
165-
{rangesMutation.isPending ? <LoadingIndicator size='S' marginEnd='size-100' /> : <></>}
165+
{rangesMutation.isPending ? <Loading mode='inline' size='S' marginEnd='size-100' /> : <></>}
166166
Save ranges
167167
</Button>
168168
</ButtonGroup>

0 commit comments

Comments
 (0)