GrafanaUI: Add new unstable ScrollContainer component (#95547)

add new unstable ScrollContainer component
This commit is contained in:
Ashley Harrison
2024-10-30 10:19:42 +00:00
committed by GitHub
parent 9b773b8501
commit 87c2bc44b2
4 changed files with 168 additions and 1 deletions
@@ -17,7 +17,7 @@ export type BorderColor = keyof GrafanaTheme2['colors']['border'] | 'error' | 's
export type BorderRadius = keyof ThemeShape['radius'];
export type BoxShadow = keyof ThemeShadows;
interface BoxProps extends FlexProps, SizeProps, Omit<React.HTMLAttributes<HTMLElement>, 'className' | 'style'> {
export interface BoxProps extends FlexProps, SizeProps, Omit<React.HTMLAttributes<HTMLElement>, 'className' | 'style'> {
// Margin props
/** Sets the property `margin` */
margin?: ResponsiveProp<ThemeSpacingTokens>;
@@ -0,0 +1,65 @@
import { css } from '@emotion/css';
import { Property } from 'csstype';
import { forwardRef, PropsWithChildren, UIEventHandler } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2 } from '../../themes';
import { Box, BoxProps } from '../Layout/Box/Box';
import { ScrollIndicators } from './ScrollIndicators';
interface Props extends Omit<BoxProps, 'display' | 'direction' | 'flex' | 'position'> {
showScrollIndicators?: boolean;
onScroll?: UIEventHandler<HTMLDivElement>;
overflowX?: Property.OverflowX;
overflowY?: Property.OverflowY;
scrollbarWidth?: Property.ScrollbarWidth;
}
export const ScrollContainer = forwardRef<HTMLDivElement, PropsWithChildren<Props>>(
(
{
children,
showScrollIndicators = false,
onScroll,
overflowX = 'auto',
overflowY = 'auto',
scrollbarWidth = 'thin',
...rest
},
ref
) => {
const styles = useStyles2(getStyles, scrollbarWidth, overflowY, overflowX);
const defaults: Partial<BoxProps> = {
maxHeight: '100%',
minHeight: 0,
};
const boxProps = { ...defaults, ...rest };
return (
<Box {...boxProps} display="flex" direction="column" flex={1} position="relative">
<div onScroll={onScroll} className={styles.scroller} ref={ref}>
{showScrollIndicators ? <ScrollIndicators>{children}</ScrollIndicators> : children}
</div>
</Box>
);
}
);
ScrollContainer.displayName = 'ScrollContainer';
const getStyles = (
theme: GrafanaTheme2,
scrollbarWidth: Props['scrollbarWidth'],
overflowY: Props['overflowY'],
overflowX: Props['overflowX']
) => ({
scroller: css({
display: 'flex',
flex: 1,
flexDirection: 'column',
overflowX,
overflowY,
scrollbarWidth,
}),
});
@@ -0,0 +1,101 @@
import { css, cx } from '@emotion/css';
import { useEffect, useRef, useState } from 'react';
import * as React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2 } from '../../themes';
export const ScrollIndicators = ({ children }: React.PropsWithChildren<{}>) => {
const [showScrollTopIndicator, setShowTopScrollIndicator] = useState(false);
const [showScrollBottomIndicator, setShowBottomScrollIndicator] = useState(false);
const scrollTopMarker = useRef<HTMLDivElement>(null);
const scrollBottomMarker = useRef<HTMLDivElement>(null);
const styles = useStyles2(getStyles);
// Here we observe the top and bottom markers to determine if we should show the scroll indicators
useEffect(() => {
const intersectionObserver = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.target === scrollTopMarker.current) {
setShowTopScrollIndicator(!entry.isIntersecting);
} else if (entry.target === scrollBottomMarker.current) {
setShowBottomScrollIndicator(!entry.isIntersecting);
}
});
});
[scrollTopMarker, scrollBottomMarker].forEach((ref) => {
if (ref.current) {
intersectionObserver.observe(ref.current);
}
});
return () => intersectionObserver.disconnect();
}, []);
return (
<>
<div
className={cx(styles.scrollIndicator, styles.scrollTopIndicator, {
[styles.scrollIndicatorVisible]: showScrollTopIndicator,
})}
/>
<div className={styles.scrollContent}>
<div ref={scrollTopMarker} className={cx(styles.scrollMarker, styles.scrollTopMarker)} />
{children}
<div ref={scrollBottomMarker} className={cx(styles.scrollMarker, styles.scrollBottomMarker)} />
</div>
<div
className={cx(styles.scrollIndicator, styles.scrollBottomIndicator, {
[styles.scrollIndicatorVisible]: showScrollBottomIndicator,
})}
/>
</>
);
};
const getStyles = (theme: GrafanaTheme2) => {
return {
scrollContent: css({
display: 'flex',
flexDirection: 'column',
flexGrow: 1,
position: 'relative',
}),
scrollIndicator: css({
height: theme.spacing(6),
left: 0,
opacity: 0,
pointerEvents: 'none',
position: 'absolute',
right: 0,
[theme.transitions.handleMotion('no-preference', 'reduce')]: {
transition: theme.transitions.create('opacity'),
},
zIndex: 1,
}),
scrollTopIndicator: css({
background: `linear-gradient(0deg, transparent, ${theme.colors.background.canvas})`,
top: 0,
}),
scrollBottomIndicator: css({
background: `linear-gradient(180deg, transparent, ${theme.colors.background.canvas})`,
bottom: 0,
}),
scrollIndicatorVisible: css({
opacity: 1,
}),
scrollMarker: css({
height: '1px',
left: 0,
pointerEvents: 'none',
position: 'absolute',
right: 0,
}),
scrollTopMarker: css({
top: 0,
}),
scrollBottomMarker: css({
bottom: 0,
}),
};
};
+1
View File
@@ -11,3 +11,4 @@
export * from './utils/skeleton';
export * from './components/Combobox/Combobox';
export * from './components/ScrollContainer/ScrollContainer';