mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	feat: add new sticky component to handle stacked stickies (#5088)
https://linear.app/unleash/issue/2-1509/discovery-stacked-sticky-elements Adds a new `Sticky` element that will attempt to stack sticky elements in the DOM in a smart way. This needs a wrapping `StickyProvider` that will keep track of sticky elements. This PR adapts a few components to use this new element: - `DemoBanner` - `FeatureOverviewSidePanel` - `DraftBanner` - `MaintenanceBanner` - `MessageBanner` Pre-existing `top` properties are taken into consideration for the top offset, so we can have nice margins like in the feature overview side panel. ### Before - Sticky elements overlap 😞  ### After - Sticky elements stack 😄 
This commit is contained in:
		
							parent
							
								
									1335da6366
								
							
						
					
					
						commit
						347c1cabbc
					
				@ -11,29 +11,22 @@ import { BannerDialog } from './BannerDialog/BannerDialog';
 | 
				
			|||||||
import { useState } from 'react';
 | 
					import { useState } from 'react';
 | 
				
			||||||
import ReactMarkdown from 'react-markdown';
 | 
					import ReactMarkdown from 'react-markdown';
 | 
				
			||||||
import { BannerVariant, IBanner } from 'interfaces/banner';
 | 
					import { BannerVariant, IBanner } from 'interfaces/banner';
 | 
				
			||||||
 | 
					import { Sticky } from 'component/common/Sticky/Sticky';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const StyledBar = styled('aside', {
 | 
					const StyledBar = styled('aside', {
 | 
				
			||||||
    shouldForwardProp: (prop) => prop !== 'variant' && prop !== 'sticky',
 | 
					    shouldForwardProp: (prop) => prop !== 'variant',
 | 
				
			||||||
})<{ variant: BannerVariant; sticky?: boolean }>(
 | 
					})<{ variant: BannerVariant }>(({ theme, variant }) => ({
 | 
				
			||||||
    ({ theme, variant, sticky }) => ({
 | 
					    display: 'flex',
 | 
				
			||||||
        position: sticky ? 'sticky' : 'relative',
 | 
					    alignItems: 'center',
 | 
				
			||||||
        zIndex: 1,
 | 
					    justifyContent: 'center',
 | 
				
			||||||
        display: 'flex',
 | 
					    padding: theme.spacing(1),
 | 
				
			||||||
        alignItems: 'center',
 | 
					    gap: theme.spacing(1),
 | 
				
			||||||
        justifyContent: 'center',
 | 
					    borderBottom: '1px solid',
 | 
				
			||||||
        padding: theme.spacing(1),
 | 
					    borderColor: theme.palette[variant].border,
 | 
				
			||||||
        gap: theme.spacing(1),
 | 
					    background: theme.palette[variant].light,
 | 
				
			||||||
        borderBottom: '1px solid',
 | 
					    color: theme.palette[variant].dark,
 | 
				
			||||||
        borderColor: theme.palette[variant].border,
 | 
					    fontSize: theme.fontSizes.smallBody,
 | 
				
			||||||
        background: theme.palette[variant].light,
 | 
					}));
 | 
				
			||||||
        color: theme.palette[variant].dark,
 | 
					 | 
				
			||||||
        fontSize: theme.fontSizes.smallBody,
 | 
					 | 
				
			||||||
        ...(sticky && {
 | 
					 | 
				
			||||||
            top: 0,
 | 
					 | 
				
			||||||
            zIndex: theme.zIndex.sticky - 100,
 | 
					 | 
				
			||||||
        }),
 | 
					 | 
				
			||||||
    }),
 | 
					 | 
				
			||||||
);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const StyledIcon = styled('div', {
 | 
					const StyledIcon = styled('div', {
 | 
				
			||||||
    shouldForwardProp: (prop) => prop !== 'variant',
 | 
					    shouldForwardProp: (prop) => prop !== 'variant',
 | 
				
			||||||
@ -62,8 +55,8 @@ export const Banner = ({ banner }: IBannerProps) => {
 | 
				
			|||||||
        dialog,
 | 
					        dialog,
 | 
				
			||||||
    } = banner;
 | 
					    } = banner;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    const bannerBar = (
 | 
				
			||||||
        <StyledBar variant={variant} sticky={sticky}>
 | 
					        <StyledBar variant={variant}>
 | 
				
			||||||
            <StyledIcon variant={variant}>
 | 
					            <StyledIcon variant={variant}>
 | 
				
			||||||
                <BannerIcon icon={icon} variant={variant} />
 | 
					                <BannerIcon icon={icon} variant={variant} />
 | 
				
			||||||
            </StyledIcon>
 | 
					            </StyledIcon>
 | 
				
			||||||
@ -84,6 +77,12 @@ export const Banner = ({ banner }: IBannerProps) => {
 | 
				
			|||||||
            </BannerDialog>
 | 
					            </BannerDialog>
 | 
				
			||||||
        </StyledBar>
 | 
					        </StyledBar>
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (sticky) {
 | 
				
			||||||
 | 
					        return <Sticky>{bannerBar}</Sticky>;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return bannerBar;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const VariantIcons = {
 | 
					const VariantIcons = {
 | 
				
			||||||
 | 
				
			|||||||
@ -8,6 +8,7 @@ import { AccessProvider } from '../providers/AccessProvider/AccessProvider';
 | 
				
			|||||||
import { AnnouncerProvider } from '../common/Announcer/AnnouncerProvider/AnnouncerProvider';
 | 
					import { AnnouncerProvider } from '../common/Announcer/AnnouncerProvider/AnnouncerProvider';
 | 
				
			||||||
import { testServerRoute, testServerSetup } from '../../utils/testServer';
 | 
					import { testServerRoute, testServerSetup } from '../../utils/testServer';
 | 
				
			||||||
import { UIProviderContainer } from '../providers/UIProvider/UIProviderContainer';
 | 
					import { UIProviderContainer } from '../providers/UIProvider/UIProviderContainer';
 | 
				
			||||||
 | 
					import { StickyProvider } from 'component/common/Sticky/StickyProvider';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const server = testServerSetup();
 | 
					const server = testServerSetup();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -227,12 +228,16 @@ const UnleashUiSetup: FC<{ path: string; pathTemplate: string }> = ({
 | 
				
			|||||||
            <MemoryRouter initialEntries={[path]}>
 | 
					            <MemoryRouter initialEntries={[path]}>
 | 
				
			||||||
                <ThemeProvider>
 | 
					                <ThemeProvider>
 | 
				
			||||||
                    <AnnouncerProvider>
 | 
					                    <AnnouncerProvider>
 | 
				
			||||||
                        <Routes>
 | 
					                        <StickyProvider>
 | 
				
			||||||
                            <Route
 | 
					                            <Routes>
 | 
				
			||||||
                                path={pathTemplate}
 | 
					                                <Route
 | 
				
			||||||
                                element={<MainLayout>{children}</MainLayout>}
 | 
					                                    path={pathTemplate}
 | 
				
			||||||
                            />
 | 
					                                    element={
 | 
				
			||||||
                        </Routes>
 | 
					                                        <MainLayout>{children}</MainLayout>
 | 
				
			||||||
 | 
					                                    }
 | 
				
			||||||
 | 
					                                />
 | 
				
			||||||
 | 
					                            </Routes>
 | 
				
			||||||
 | 
					                        </StickyProvider>
 | 
				
			||||||
                    </AnnouncerProvider>
 | 
					                    </AnnouncerProvider>
 | 
				
			||||||
                </ThemeProvider>
 | 
					                </ThemeProvider>
 | 
				
			||||||
            </MemoryRouter>
 | 
					            </MemoryRouter>
 | 
				
			||||||
 | 
				
			|||||||
@ -10,6 +10,7 @@ import { FC } from 'react';
 | 
				
			|||||||
import { IPermission } from '../../interfaces/user';
 | 
					import { IPermission } from '../../interfaces/user';
 | 
				
			||||||
import { SWRConfig } from 'swr';
 | 
					import { SWRConfig } from 'swr';
 | 
				
			||||||
import { ProjectMode } from '../project/Project/hooks/useProjectEnterpriseSettingsForm';
 | 
					import { ProjectMode } from '../project/Project/hooks/useProjectEnterpriseSettingsForm';
 | 
				
			||||||
 | 
					import { StickyProvider } from 'component/common/Sticky/StickyProvider';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const server = testServerSetup();
 | 
					const server = testServerSetup();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -186,9 +187,14 @@ const UnleashUiSetup: FC<{ path: string; pathTemplate: string }> = ({
 | 
				
			|||||||
                <MemoryRouter initialEntries={[path]}>
 | 
					                <MemoryRouter initialEntries={[path]}>
 | 
				
			||||||
                    <ThemeProvider>
 | 
					                    <ThemeProvider>
 | 
				
			||||||
                        <AnnouncerProvider>
 | 
					                        <AnnouncerProvider>
 | 
				
			||||||
                            <Routes>
 | 
					                            <StickyProvider>
 | 
				
			||||||
                                <Route path={pathTemplate} element={children} />
 | 
					                                <Routes>
 | 
				
			||||||
                            </Routes>
 | 
					                                    <Route
 | 
				
			||||||
 | 
					                                        path={pathTemplate}
 | 
				
			||||||
 | 
					                                        element={children}
 | 
				
			||||||
 | 
					                                    />
 | 
				
			||||||
 | 
					                                </Routes>
 | 
				
			||||||
 | 
					                            </StickyProvider>
 | 
				
			||||||
                        </AnnouncerProvider>
 | 
					                        </AnnouncerProvider>
 | 
				
			||||||
                    </ThemeProvider>
 | 
					                    </ThemeProvider>
 | 
				
			||||||
                </MemoryRouter>
 | 
					                </MemoryRouter>
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										112
									
								
								frontend/src/component/common/Sticky/Sticky.test.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								frontend/src/component/common/Sticky/Sticky.test.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,112 @@
 | 
				
			|||||||
 | 
					import { render, screen, cleanup } from '@testing-library/react';
 | 
				
			||||||
 | 
					import { Sticky } from './Sticky';
 | 
				
			||||||
 | 
					import { IStickyContext, StickyContext } from './StickyContext';
 | 
				
			||||||
 | 
					import { vi, expect } from 'vitest';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe('Sticky component', () => {
 | 
				
			||||||
 | 
					    let originalConsoleError: () => void;
 | 
				
			||||||
 | 
					    let mockRegisterStickyItem: () => void;
 | 
				
			||||||
 | 
					    let mockUnregisterStickyItem: () => void;
 | 
				
			||||||
 | 
					    let mockGetTopOffset: () => number;
 | 
				
			||||||
 | 
					    let mockContextValue: IStickyContext;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    beforeEach(() => {
 | 
				
			||||||
 | 
					        originalConsoleError = console.error;
 | 
				
			||||||
 | 
					        console.error = vi.fn();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        mockRegisterStickyItem = vi.fn();
 | 
				
			||||||
 | 
					        mockUnregisterStickyItem = vi.fn();
 | 
				
			||||||
 | 
					        mockGetTopOffset = vi.fn(() => 10);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        mockContextValue = {
 | 
				
			||||||
 | 
					            registerStickyItem: mockRegisterStickyItem,
 | 
				
			||||||
 | 
					            unregisterStickyItem: mockUnregisterStickyItem,
 | 
				
			||||||
 | 
					            getTopOffset: mockGetTopOffset,
 | 
				
			||||||
 | 
					            stickyItems: [],
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    afterEach(() => {
 | 
				
			||||||
 | 
					        cleanup();
 | 
				
			||||||
 | 
					        console.error = originalConsoleError;
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('renders correctly within StickyContext', () => {
 | 
				
			||||||
 | 
					        render(
 | 
				
			||||||
 | 
					            <StickyContext.Provider value={mockContextValue}>
 | 
				
			||||||
 | 
					                <Sticky>Content</Sticky>
 | 
				
			||||||
 | 
					            </StickyContext.Provider>,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        expect(screen.getByText('Content')).toBeInTheDocument();
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('throws error when not wrapped in StickyContext', () => {
 | 
				
			||||||
 | 
					        console.error = vi.fn();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        expect(() => render(<Sticky>Content</Sticky>)).toThrow(
 | 
				
			||||||
 | 
					            'Sticky component must be used within a StickyProvider',
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('applies sticky positioning', () => {
 | 
				
			||||||
 | 
					        render(
 | 
				
			||||||
 | 
					            <StickyContext.Provider value={mockContextValue}>
 | 
				
			||||||
 | 
					                <Sticky>Content</Sticky>
 | 
				
			||||||
 | 
					            </StickyContext.Provider>,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const stickyElement = screen.getByText('Content');
 | 
				
			||||||
 | 
					        expect(stickyElement).toHaveStyle({ position: 'sticky' });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('registers and unregisters sticky item on mount/unmount', () => {
 | 
				
			||||||
 | 
					        const { unmount } = render(
 | 
				
			||||||
 | 
					            <StickyContext.Provider value={mockContextValue}>
 | 
				
			||||||
 | 
					                <Sticky>Content</Sticky>
 | 
				
			||||||
 | 
					            </StickyContext.Provider>,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        expect(mockRegisterStickyItem).toHaveBeenCalledTimes(1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        unmount();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        expect(mockUnregisterStickyItem).toHaveBeenCalledTimes(1);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('correctly sets the top value when mounted', async () => {
 | 
				
			||||||
 | 
					        render(
 | 
				
			||||||
 | 
					            <StickyContext.Provider value={mockContextValue}>
 | 
				
			||||||
 | 
					                <Sticky>Content</Sticky>
 | 
				
			||||||
 | 
					            </StickyContext.Provider>,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const stickyElement = await screen.findByText('Content');
 | 
				
			||||||
 | 
					        expect(stickyElement).toHaveStyle({ top: '10px' });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('updates top offset when stickyItems changes', async () => {
 | 
				
			||||||
 | 
					        const { rerender } = render(
 | 
				
			||||||
 | 
					            <StickyContext.Provider value={mockContextValue}>
 | 
				
			||||||
 | 
					                <Sticky>Content</Sticky>
 | 
				
			||||||
 | 
					            </StickyContext.Provider>,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let stickyElement = await screen.findByText('Content');
 | 
				
			||||||
 | 
					        expect(stickyElement).toHaveStyle({ top: '10px' });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const updatedMockContextValue = {
 | 
				
			||||||
 | 
					            ...mockContextValue,
 | 
				
			||||||
 | 
					            getTopOffset: vi.fn(() => 20),
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        rerender(
 | 
				
			||||||
 | 
					            <StickyContext.Provider value={updatedMockContextValue}>
 | 
				
			||||||
 | 
					                <Sticky>Content</Sticky>
 | 
				
			||||||
 | 
					            </StickyContext.Provider>,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        stickyElement = await screen.findByText('Content');
 | 
				
			||||||
 | 
					        expect(stickyElement).toHaveStyle({ top: '20px' });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										80
									
								
								frontend/src/component/common/Sticky/Sticky.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								frontend/src/component/common/Sticky/Sticky.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,80 @@
 | 
				
			|||||||
 | 
					import {
 | 
				
			||||||
 | 
					    HTMLAttributes,
 | 
				
			||||||
 | 
					    ReactNode,
 | 
				
			||||||
 | 
					    useContext,
 | 
				
			||||||
 | 
					    useEffect,
 | 
				
			||||||
 | 
					    useRef,
 | 
				
			||||||
 | 
					    useState,
 | 
				
			||||||
 | 
					} from 'react';
 | 
				
			||||||
 | 
					import { StickyContext } from './StickyContext';
 | 
				
			||||||
 | 
					import { styled } from '@mui/material';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const StyledSticky = styled('div', {
 | 
				
			||||||
 | 
					    shouldForwardProp: (prop) => prop !== 'top',
 | 
				
			||||||
 | 
					})<{ top?: number }>(({ theme, top }) => ({
 | 
				
			||||||
 | 
					    position: 'sticky',
 | 
				
			||||||
 | 
					    zIndex: theme.zIndex.sticky - 100,
 | 
				
			||||||
 | 
					    ...(top !== undefined
 | 
				
			||||||
 | 
					        ? {
 | 
				
			||||||
 | 
					              '&': {
 | 
				
			||||||
 | 
					                  top,
 | 
				
			||||||
 | 
					              },
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        : {}),
 | 
				
			||||||
 | 
					}));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface IStickyProps extends HTMLAttributes<HTMLDivElement> {
 | 
				
			||||||
 | 
					    children: ReactNode;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const Sticky = ({ children, ...props }: IStickyProps) => {
 | 
				
			||||||
 | 
					    const context = useContext(StickyContext);
 | 
				
			||||||
 | 
					    const ref = useRef<HTMLDivElement>(null);
 | 
				
			||||||
 | 
					    const [initialTopOffset, setInitialTopOffset] = useState<number | null>(
 | 
				
			||||||
 | 
					        null,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    const [top, setTop] = useState<number>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!context) {
 | 
				
			||||||
 | 
					        throw new Error(
 | 
				
			||||||
 | 
					            'Sticky component must be used within a StickyProvider',
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const { registerStickyItem, unregisterStickyItem, getTopOffset } = context;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    useEffect(() => {
 | 
				
			||||||
 | 
					        // We should only set the initial top offset once - when the component is mounted
 | 
				
			||||||
 | 
					        // This value will be set based on the initial top that was set for this component
 | 
				
			||||||
 | 
					        // After that, the top will be calculated based on the height of the previous sticky items + this initial top offset
 | 
				
			||||||
 | 
					        if (ref.current && initialTopOffset === null) {
 | 
				
			||||||
 | 
					            setInitialTopOffset(
 | 
				
			||||||
 | 
					                parseInt(getComputedStyle(ref.current).getPropertyValue('top')),
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    useEffect(() => {
 | 
				
			||||||
 | 
					        // (Re)calculate the top offset based on the sticky items
 | 
				
			||||||
 | 
					        setTop(getTopOffset(ref) + (initialTopOffset || 0));
 | 
				
			||||||
 | 
					    }, [getTopOffset, initialTopOffset]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    useEffect(() => {
 | 
				
			||||||
 | 
					        // We should register the sticky item when it is mounted and unregister it when it is unmounted
 | 
				
			||||||
 | 
					        if (!ref.current) {
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        registerStickyItem(ref);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return () => {
 | 
				
			||||||
 | 
					            unregisterStickyItem(ref);
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					    }, [ref, registerStickyItem, unregisterStickyItem]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					        <StyledSticky ref={ref} top={top} {...props}>
 | 
				
			||||||
 | 
					            {children}
 | 
				
			||||||
 | 
					        </StyledSticky>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										12
									
								
								frontend/src/component/common/Sticky/StickyContext.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								frontend/src/component/common/Sticky/StickyContext.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,12 @@
 | 
				
			|||||||
 | 
					import { RefObject, createContext } from 'react';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface IStickyContext {
 | 
				
			||||||
 | 
					    stickyItems: RefObject<HTMLDivElement>[];
 | 
				
			||||||
 | 
					    registerStickyItem: (ref: RefObject<HTMLDivElement>) => void;
 | 
				
			||||||
 | 
					    unregisterStickyItem: (ref: RefObject<HTMLDivElement>) => void;
 | 
				
			||||||
 | 
					    getTopOffset: (ref: RefObject<HTMLDivElement>) => number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const StickyContext = createContext<IStickyContext | undefined>(
 | 
				
			||||||
 | 
					    undefined,
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
							
								
								
									
										160
									
								
								frontend/src/component/common/Sticky/StickyProvider.test.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										160
									
								
								frontend/src/component/common/Sticky/StickyProvider.test.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,160 @@
 | 
				
			|||||||
 | 
					import { render, cleanup } from '@testing-library/react';
 | 
				
			||||||
 | 
					import { StickyProvider } from './StickyProvider';
 | 
				
			||||||
 | 
					import { IStickyContext, StickyContext } from './StickyContext';
 | 
				
			||||||
 | 
					import { expect } from 'vitest';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const defaultGetBoundingClientRect = {
 | 
				
			||||||
 | 
					    width: 0,
 | 
				
			||||||
 | 
					    height: 0,
 | 
				
			||||||
 | 
					    top: 0,
 | 
				
			||||||
 | 
					    left: 0,
 | 
				
			||||||
 | 
					    bottom: 0,
 | 
				
			||||||
 | 
					    right: 0,
 | 
				
			||||||
 | 
					    x: 0,
 | 
				
			||||||
 | 
					    y: 0,
 | 
				
			||||||
 | 
					    toJSON() {},
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe('StickyProvider component', () => {
 | 
				
			||||||
 | 
					    afterEach(cleanup);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('provides the sticky context with expected functions', () => {
 | 
				
			||||||
 | 
					        let receivedContext = null;
 | 
				
			||||||
 | 
					        render(
 | 
				
			||||||
 | 
					            <StickyProvider>
 | 
				
			||||||
 | 
					                <StickyContext.Consumer>
 | 
				
			||||||
 | 
					                    {(context) => {
 | 
				
			||||||
 | 
					                        receivedContext = context;
 | 
				
			||||||
 | 
					                        return null;
 | 
				
			||||||
 | 
					                    }}
 | 
				
			||||||
 | 
					                </StickyContext.Consumer>
 | 
				
			||||||
 | 
					            </StickyProvider>,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        expect(receivedContext).not.toBeNull();
 | 
				
			||||||
 | 
					        expect(receivedContext).toHaveProperty('stickyItems');
 | 
				
			||||||
 | 
					        expect(receivedContext).toHaveProperty('registerStickyItem');
 | 
				
			||||||
 | 
					        expect(receivedContext).toHaveProperty('unregisterStickyItem');
 | 
				
			||||||
 | 
					        expect(receivedContext).toHaveProperty('getTopOffset');
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('registers and unregisters sticky items', () => {
 | 
				
			||||||
 | 
					        let contextValues: IStickyContext | undefined;
 | 
				
			||||||
 | 
					        const refMock = { current: document.createElement('div') };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const { rerender } = render(
 | 
				
			||||||
 | 
					            <StickyProvider>
 | 
				
			||||||
 | 
					                <StickyContext.Consumer>
 | 
				
			||||||
 | 
					                    {(context) => {
 | 
				
			||||||
 | 
					                        contextValues = context;
 | 
				
			||||||
 | 
					                        return null;
 | 
				
			||||||
 | 
					                    }}
 | 
				
			||||||
 | 
					                </StickyContext.Consumer>
 | 
				
			||||||
 | 
					            </StickyProvider>,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        contextValues?.registerStickyItem(refMock);
 | 
				
			||||||
 | 
					        rerender(
 | 
				
			||||||
 | 
					            <StickyProvider>
 | 
				
			||||||
 | 
					                <StickyContext.Consumer>
 | 
				
			||||||
 | 
					                    {(context) => {
 | 
				
			||||||
 | 
					                        contextValues = context;
 | 
				
			||||||
 | 
					                        return null;
 | 
				
			||||||
 | 
					                    }}
 | 
				
			||||||
 | 
					                </StickyContext.Consumer>
 | 
				
			||||||
 | 
					            </StickyProvider>,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        expect(contextValues?.stickyItems).toContain(refMock);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        contextValues?.unregisterStickyItem(refMock);
 | 
				
			||||||
 | 
					        rerender(
 | 
				
			||||||
 | 
					            <StickyProvider>
 | 
				
			||||||
 | 
					                <StickyContext.Consumer>
 | 
				
			||||||
 | 
					                    {(context) => {
 | 
				
			||||||
 | 
					                        contextValues = context;
 | 
				
			||||||
 | 
					                        return null;
 | 
				
			||||||
 | 
					                    }}
 | 
				
			||||||
 | 
					                </StickyContext.Consumer>
 | 
				
			||||||
 | 
					            </StickyProvider>,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        expect(contextValues?.stickyItems).not.toContain(refMock);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('sorts sticky items based on their DOM position', () => {
 | 
				
			||||||
 | 
					        let contextValues: IStickyContext | undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const refMockA = { current: document.createElement('div') };
 | 
				
			||||||
 | 
					        const refMockB = { current: document.createElement('div') };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        refMockA.current.getBoundingClientRect = () => ({
 | 
				
			||||||
 | 
					            ...defaultGetBoundingClientRect,
 | 
				
			||||||
 | 
					            top: 200,
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        refMockB.current.getBoundingClientRect = () => ({
 | 
				
			||||||
 | 
					            ...defaultGetBoundingClientRect,
 | 
				
			||||||
 | 
					            top: 100,
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        render(
 | 
				
			||||||
 | 
					            <StickyProvider>
 | 
				
			||||||
 | 
					                <StickyContext.Consumer>
 | 
				
			||||||
 | 
					                    {(context) => {
 | 
				
			||||||
 | 
					                        contextValues = context;
 | 
				
			||||||
 | 
					                        return null;
 | 
				
			||||||
 | 
					                    }}
 | 
				
			||||||
 | 
					                </StickyContext.Consumer>
 | 
				
			||||||
 | 
					            </StickyProvider>,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        contextValues?.registerStickyItem(refMockA);
 | 
				
			||||||
 | 
					        contextValues?.registerStickyItem(refMockB);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        expect(contextValues?.stickyItems[0]).toBe(refMockB);
 | 
				
			||||||
 | 
					        expect(contextValues?.stickyItems[1]).toBe(refMockA);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('calculates top offset correctly', () => {
 | 
				
			||||||
 | 
					        let contextValues: IStickyContext | undefined;
 | 
				
			||||||
 | 
					        const refMockA = { current: document.createElement('div') };
 | 
				
			||||||
 | 
					        const refMockB = { current: document.createElement('div') };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        refMockA.current.getBoundingClientRect = () => ({
 | 
				
			||||||
 | 
					            ...defaultGetBoundingClientRect,
 | 
				
			||||||
 | 
					            height: 100,
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        refMockB.current.getBoundingClientRect = () => ({
 | 
				
			||||||
 | 
					            ...defaultGetBoundingClientRect,
 | 
				
			||||||
 | 
					            height: 200,
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const { rerender } = render(
 | 
				
			||||||
 | 
					            <StickyProvider>
 | 
				
			||||||
 | 
					                <StickyContext.Consumer>
 | 
				
			||||||
 | 
					                    {(context) => {
 | 
				
			||||||
 | 
					                        contextValues = context;
 | 
				
			||||||
 | 
					                        return null;
 | 
				
			||||||
 | 
					                    }}
 | 
				
			||||||
 | 
					                </StickyContext.Consumer>
 | 
				
			||||||
 | 
					            </StickyProvider>,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        contextValues?.registerStickyItem(refMockA);
 | 
				
			||||||
 | 
					        contextValues?.registerStickyItem(refMockB);
 | 
				
			||||||
 | 
					        rerender(
 | 
				
			||||||
 | 
					            <StickyProvider>
 | 
				
			||||||
 | 
					                <StickyContext.Consumer>
 | 
				
			||||||
 | 
					                    {(context) => {
 | 
				
			||||||
 | 
					                        contextValues = context;
 | 
				
			||||||
 | 
					                        return null;
 | 
				
			||||||
 | 
					                    }}
 | 
				
			||||||
 | 
					                </StickyContext.Consumer>
 | 
				
			||||||
 | 
					            </StickyProvider>,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const topOffset = contextValues?.getTopOffset(refMockB);
 | 
				
			||||||
 | 
					        expect(topOffset).toBe(100);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										133
									
								
								frontend/src/component/common/Sticky/StickyProvider.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										133
									
								
								frontend/src/component/common/Sticky/StickyProvider.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,133 @@
 | 
				
			|||||||
 | 
					import { useState, useCallback, ReactNode, RefObject, useEffect } from 'react';
 | 
				
			||||||
 | 
					import { StickyContext } from './StickyContext';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface IStickyProviderProps {
 | 
				
			||||||
 | 
					    children: ReactNode;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const StickyProvider = ({ children }: IStickyProviderProps) => {
 | 
				
			||||||
 | 
					    const [stickyItems, setStickyItems] = useState<RefObject<HTMLDivElement>[]>(
 | 
				
			||||||
 | 
					        [],
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    const [resizeListeners, setResizeListeners] = useState(
 | 
				
			||||||
 | 
					        new Set<RefObject<HTMLDivElement>>(),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const registerStickyItem = useCallback(
 | 
				
			||||||
 | 
					        (item: RefObject<HTMLDivElement>) => {
 | 
				
			||||||
 | 
					            setStickyItems((prevItems) => {
 | 
				
			||||||
 | 
					                // We should only register a new item if it is not already registered
 | 
				
			||||||
 | 
					                if (!prevItems.includes(item)) {
 | 
				
			||||||
 | 
					                    // Register resize listener for the item
 | 
				
			||||||
 | 
					                    registerResizeListener(item);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    const newItems = [...prevItems, item];
 | 
				
			||||||
 | 
					                    // We should try to sort the items by their top on the viewport, so that their order in the DOM is the same as their order in the array
 | 
				
			||||||
 | 
					                    return newItems.sort((a, b) => {
 | 
				
			||||||
 | 
					                        const elementA = a.current;
 | 
				
			||||||
 | 
					                        const elementB = b.current;
 | 
				
			||||||
 | 
					                        if (elementA && elementB) {
 | 
				
			||||||
 | 
					                            return (
 | 
				
			||||||
 | 
					                                elementA.getBoundingClientRect().top -
 | 
				
			||||||
 | 
					                                elementB.getBoundingClientRect().top
 | 
				
			||||||
 | 
					                            );
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                        return 0;
 | 
				
			||||||
 | 
					                    });
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                return prevItems;
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        [],
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const unregisterStickyItem = useCallback(
 | 
				
			||||||
 | 
					        (ref: RefObject<HTMLDivElement>) => {
 | 
				
			||||||
 | 
					            unregisterResizeListener(ref);
 | 
				
			||||||
 | 
					            setStickyItems((prev) => prev.filter((item) => item !== ref));
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        [],
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const registerResizeListener = useCallback(
 | 
				
			||||||
 | 
					        (ref: RefObject<HTMLDivElement>) => {
 | 
				
			||||||
 | 
					            setResizeListeners((prev) => new Set(prev).add(ref));
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        [],
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const unregisterResizeListener = useCallback(
 | 
				
			||||||
 | 
					        (ref: RefObject<HTMLDivElement>) => {
 | 
				
			||||||
 | 
					            setResizeListeners((prev) => {
 | 
				
			||||||
 | 
					                const newListeners = new Set(prev);
 | 
				
			||||||
 | 
					                newListeners.delete(ref);
 | 
				
			||||||
 | 
					                return newListeners;
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        [],
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const getTopOffset = useCallback(
 | 
				
			||||||
 | 
					        (ref: RefObject<HTMLDivElement>) => {
 | 
				
			||||||
 | 
					            if (!stickyItems.some((item) => item === ref)) {
 | 
				
			||||||
 | 
					                // Return 0 in case the item is not registered yet
 | 
				
			||||||
 | 
					                return 0;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            const stickyItemsUpToOurItem = stickyItems.slice(
 | 
				
			||||||
 | 
					                0,
 | 
				
			||||||
 | 
					                stickyItems.findIndex((item) => item === ref),
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					            return stickyItemsUpToOurItem.reduce((acc, item) => {
 | 
				
			||||||
 | 
					                if (item === ref) {
 | 
				
			||||||
 | 
					                    // We should not include the current item in the calculation
 | 
				
			||||||
 | 
					                    return acc;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // Accumulate the height of all sticky items above our item
 | 
				
			||||||
 | 
					                const itemHeight =
 | 
				
			||||||
 | 
					                    item.current?.getBoundingClientRect().height || 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                return acc + itemHeight;
 | 
				
			||||||
 | 
					            }, 0);
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        [stickyItems],
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    useEffect(() => {
 | 
				
			||||||
 | 
					        const resizeObserver = new ResizeObserver(() => {
 | 
				
			||||||
 | 
					            // We should recalculate top offsets whenever there's a resize
 | 
				
			||||||
 | 
					            // This will trigger the dependency in `getTopOffset` and recalculate the top offsets in the Sticky components
 | 
				
			||||||
 | 
					            setStickyItems((prev) => [...prev]);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        resizeListeners.forEach((item) => {
 | 
				
			||||||
 | 
					            if (item.current) {
 | 
				
			||||||
 | 
					                resizeObserver.observe(item.current);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return () => {
 | 
				
			||||||
 | 
					            if (resizeListeners.size > 0) {
 | 
				
			||||||
 | 
					                resizeListeners.forEach((item) => {
 | 
				
			||||||
 | 
					                    if (item.current) {
 | 
				
			||||||
 | 
					                        resizeObserver.unobserve(item.current);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					    }, [resizeListeners]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					        <StickyContext.Provider
 | 
				
			||||||
 | 
					            value={{
 | 
				
			||||||
 | 
					                stickyItems,
 | 
				
			||||||
 | 
					                registerStickyItem,
 | 
				
			||||||
 | 
					                unregisterStickyItem,
 | 
				
			||||||
 | 
					                getTopOffset,
 | 
				
			||||||
 | 
					            }}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					            {children}
 | 
				
			||||||
 | 
					        </StickyContext.Provider>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
@ -1,9 +1,8 @@
 | 
				
			|||||||
import { Button, styled } from '@mui/material';
 | 
					import { Button, styled } from '@mui/material';
 | 
				
			||||||
 | 
					import { Sticky } from 'component/common/Sticky/Sticky';
 | 
				
			||||||
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
 | 
					import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const StyledBanner = styled('div')(({ theme }) => ({
 | 
					const StyledBanner = styled(Sticky)(({ theme }) => ({
 | 
				
			||||||
    position: 'sticky',
 | 
					 | 
				
			||||||
    top: 0,
 | 
					 | 
				
			||||||
    zIndex: theme.zIndex.sticky,
 | 
					    zIndex: theme.zIndex.sticky,
 | 
				
			||||||
    display: 'flex',
 | 
					    display: 'flex',
 | 
				
			||||||
    gap: theme.spacing(1),
 | 
					    gap: theme.spacing(1),
 | 
				
			||||||
 | 
				
			|||||||
@ -5,9 +5,9 @@ import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
 | 
				
			|||||||
import { FeatureOverviewSidePanelDetails } from './FeatureOverviewSidePanelDetails/FeatureOverviewSidePanelDetails';
 | 
					import { FeatureOverviewSidePanelDetails } from './FeatureOverviewSidePanelDetails/FeatureOverviewSidePanelDetails';
 | 
				
			||||||
import { FeatureOverviewSidePanelEnvironmentSwitches } from './FeatureOverviewSidePanelEnvironmentSwitches/FeatureOverviewSidePanelEnvironmentSwitches';
 | 
					import { FeatureOverviewSidePanelEnvironmentSwitches } from './FeatureOverviewSidePanelEnvironmentSwitches/FeatureOverviewSidePanelEnvironmentSwitches';
 | 
				
			||||||
import { FeatureOverviewSidePanelTags } from './FeatureOverviewSidePanelTags/FeatureOverviewSidePanelTags';
 | 
					import { FeatureOverviewSidePanelTags } from './FeatureOverviewSidePanelTags/FeatureOverviewSidePanelTags';
 | 
				
			||||||
 | 
					import { Sticky } from 'component/common/Sticky/Sticky';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const StyledContainer = styled('div')(({ theme }) => ({
 | 
					const StyledContainer = styled(Sticky)(({ theme }) => ({
 | 
				
			||||||
    position: 'sticky',
 | 
					 | 
				
			||||||
    top: theme.spacing(2),
 | 
					    top: theme.spacing(2),
 | 
				
			||||||
    borderRadius: theme.shape.borderRadiusLarge,
 | 
					    borderRadius: theme.shape.borderRadiusLarge,
 | 
				
			||||||
    backgroundColor: theme.palette.background.paper,
 | 
					    backgroundColor: theme.palette.background.paper,
 | 
				
			||||||
 | 
				
			|||||||
@ -5,6 +5,7 @@ import { ChangeRequestSidebar } from 'component/changeRequest/ChangeRequestSideb
 | 
				
			|||||||
import { usePendingChangeRequests } from 'hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests';
 | 
					import { usePendingChangeRequests } from 'hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests';
 | 
				
			||||||
import { IChangeRequest } from 'component/changeRequest/changeRequest.types';
 | 
					import { IChangeRequest } from 'component/changeRequest/changeRequest.types';
 | 
				
			||||||
import { changesCount } from 'component/changeRequest/changesCount';
 | 
					import { changesCount } from 'component/changeRequest/changesCount';
 | 
				
			||||||
 | 
					import { Sticky } from 'component/common/Sticky/Sticky';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface IDraftBannerProps {
 | 
					interface IDraftBannerProps {
 | 
				
			||||||
    project: string;
 | 
					    project: string;
 | 
				
			||||||
@ -98,10 +99,7 @@ const DraftBannerContent: FC<{
 | 
				
			|||||||
    );
 | 
					    );
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const StickyBanner = styled(Box)(({ theme }) => ({
 | 
					const StickyBanner = styled(Sticky)(({ theme }) => ({
 | 
				
			||||||
    position: 'sticky',
 | 
					 | 
				
			||||||
    top: -1,
 | 
					 | 
				
			||||||
    zIndex: 250 /* has to lower than header.zIndex and higher than body.zIndex */,
 | 
					 | 
				
			||||||
    borderTop: `1px solid ${theme.palette.warning.border}`,
 | 
					    borderTop: `1px solid ${theme.palette.warning.border}`,
 | 
				
			||||||
    borderBottom: `1px solid ${theme.palette.warning.border}`,
 | 
					    borderBottom: `1px solid ${theme.palette.warning.border}`,
 | 
				
			||||||
    color: theme.palette.warning.contrastText,
 | 
					    color: theme.palette.warning.contrastText,
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,6 @@
 | 
				
			|||||||
import { styled } from '@mui/material';
 | 
					import { styled } from '@mui/material';
 | 
				
			||||||
import { ErrorOutlineRounded } from '@mui/icons-material';
 | 
					import { ErrorOutlineRounded } from '@mui/icons-material';
 | 
				
			||||||
 | 
					import { Sticky } from 'component/common/Sticky/Sticky';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const StyledErrorRoundedIcon = styled(ErrorOutlineRounded)(({ theme }) => ({
 | 
					const StyledErrorRoundedIcon = styled(ErrorOutlineRounded)(({ theme }) => ({
 | 
				
			||||||
    color: theme.palette.error.main,
 | 
					    color: theme.palette.error.main,
 | 
				
			||||||
@ -8,7 +9,7 @@ const StyledErrorRoundedIcon = styled(ErrorOutlineRounded)(({ theme }) => ({
 | 
				
			|||||||
    marginRight: theme.spacing(1),
 | 
					    marginRight: theme.spacing(1),
 | 
				
			||||||
}));
 | 
					}));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const StyledDiv = styled('div')(({ theme }) => ({
 | 
					const StyledDiv = styled(Sticky)(({ theme }) => ({
 | 
				
			||||||
    display: 'flex',
 | 
					    display: 'flex',
 | 
				
			||||||
    fontSize: theme.fontSizes.smallBody,
 | 
					    fontSize: theme.fontSizes.smallBody,
 | 
				
			||||||
    justifyContent: 'center',
 | 
					    justifyContent: 'center',
 | 
				
			||||||
@ -18,8 +19,6 @@ const StyledDiv = styled('div')(({ theme }) => ({
 | 
				
			|||||||
    height: '65px',
 | 
					    height: '65px',
 | 
				
			||||||
    borderBottom: `1px solid ${theme.palette.error.border}`,
 | 
					    borderBottom: `1px solid ${theme.palette.error.border}`,
 | 
				
			||||||
    whiteSpace: 'pre-wrap',
 | 
					    whiteSpace: 'pre-wrap',
 | 
				
			||||||
    position: 'sticky',
 | 
					 | 
				
			||||||
    top: 0,
 | 
					 | 
				
			||||||
    zIndex: theme.zIndex.sticky - 100,
 | 
					    zIndex: theme.zIndex.sticky - 100,
 | 
				
			||||||
}));
 | 
					}));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -13,6 +13,7 @@ import { FeedbackCESProvider } from 'component/feedback/FeedbackCESContext/Feedb
 | 
				
			|||||||
import { AnnouncerProvider } from 'component/common/Announcer/AnnouncerProvider/AnnouncerProvider';
 | 
					import { AnnouncerProvider } from 'component/common/Announcer/AnnouncerProvider/AnnouncerProvider';
 | 
				
			||||||
import { InstanceStatus } from 'component/common/InstanceStatus/InstanceStatus';
 | 
					import { InstanceStatus } from 'component/common/InstanceStatus/InstanceStatus';
 | 
				
			||||||
import { UIProviderContainer } from 'component/providers/UIProvider/UIProviderContainer';
 | 
					import { UIProviderContainer } from 'component/providers/UIProvider/UIProviderContainer';
 | 
				
			||||||
 | 
					import { StickyProvider } from 'component/common/Sticky/StickyProvider';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
window.global ||= window;
 | 
					window.global ||= window;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -23,10 +24,12 @@ ReactDOM.render(
 | 
				
			|||||||
                <ThemeProvider>
 | 
					                <ThemeProvider>
 | 
				
			||||||
                    <AnnouncerProvider>
 | 
					                    <AnnouncerProvider>
 | 
				
			||||||
                        <FeedbackCESProvider>
 | 
					                        <FeedbackCESProvider>
 | 
				
			||||||
                            <InstanceStatus>
 | 
					                            <StickyProvider>
 | 
				
			||||||
                                <ScrollTop />
 | 
					                                <InstanceStatus>
 | 
				
			||||||
                                <App />
 | 
					                                    <ScrollTop />
 | 
				
			||||||
                            </InstanceStatus>
 | 
					                                    <App />
 | 
				
			||||||
 | 
					                                </InstanceStatus>
 | 
				
			||||||
 | 
					                            </StickyProvider>
 | 
				
			||||||
                        </FeedbackCESProvider>
 | 
					                        </FeedbackCESProvider>
 | 
				
			||||||
                    </AnnouncerProvider>
 | 
					                    </AnnouncerProvider>
 | 
				
			||||||
                </ThemeProvider>
 | 
					                </ThemeProvider>
 | 
				
			||||||
 | 
				
			|||||||
@ -2,4 +2,14 @@ import '@testing-library/jest-dom';
 | 
				
			|||||||
import 'whatwg-fetch';
 | 
					import 'whatwg-fetch';
 | 
				
			||||||
import 'regenerator-runtime';
 | 
					import 'regenerator-runtime';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ResizeObserver {
 | 
				
			||||||
 | 
					    observe() {}
 | 
				
			||||||
 | 
					    unobserve() {}
 | 
				
			||||||
 | 
					    disconnect() {}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if (!window.ResizeObserver) {
 | 
				
			||||||
 | 
					    window.ResizeObserver = ResizeObserver;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
process.env.TZ = 'UTC';
 | 
					process.env.TZ = 'UTC';
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user