GrafanaUI: Add new unstable ScrollContainer component (#95547)
add new unstable ScrollContainer component
This commit is contained in:
@@ -17,7 +17,7 @@ export type BorderColor = keyof GrafanaTheme2['colors']['border'] | 'error' | 's
|
|||||||
export type BorderRadius = keyof ThemeShape['radius'];
|
export type BorderRadius = keyof ThemeShape['radius'];
|
||||||
export type BoxShadow = keyof ThemeShadows;
|
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
|
// Margin props
|
||||||
/** Sets the property `margin` */
|
/** Sets the property `margin` */
|
||||||
margin?: ResponsiveProp<ThemeSpacingTokens>;
|
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,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -11,3 +11,4 @@
|
|||||||
|
|
||||||
export * from './utils/skeleton';
|
export * from './utils/skeleton';
|
||||||
export * from './components/Combobox/Combobox';
|
export * from './components/Combobox/Combobox';
|
||||||
|
export * from './components/ScrollContainer/ScrollContainer';
|
||||||
|
|||||||
Reference in New Issue
Block a user