1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-06 00:07:44 +01:00
unleash.unleash/frontend/src/component/common/Sticky/Sticky.tsx
Nuno Góis 347c1cabbc
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 😞

![image](https://github.com/Unleash/unleash/assets/14320932/dd6fa188-6774-4afb-86fd-0eefb9aba93e)

### After - Sticky elements stack 😄 

![image](https://github.com/Unleash/unleash/assets/14320932/c73a84ab-7133-448f-9df6-69bd4c5330c2)
2023-10-19 15:50:37 +01:00

81 lines
2.3 KiB
TypeScript

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>
);
};