diff --git a/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx b/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx index e7f62d72e96..a614a840b45 100644 --- a/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx +++ b/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx @@ -1,5 +1,4 @@ -import { css } from '@emotion/css'; -import classNames from 'classnames'; +import { css, cx } from '@emotion/css'; import React, { FC, RefCallback, useCallback, useEffect, useRef } from 'react'; import Scrollbars, { positionValues } from 'react-custom-scrollbars-2'; @@ -7,6 +6,8 @@ import { GrafanaTheme2 } from '@grafana/data'; import { useStyles2 } from '../../themes'; +import { ScrollIndicators } from './ScrollIndicators'; + export type ScrollbarPosition = positionValues; interface Props { @@ -21,6 +22,7 @@ interface Props { scrollRefCallback?: RefCallback; scrollTop?: number; setScrollTop?: (position: ScrollbarPosition) => void; + showScrollIndicators?: boolean; autoHeightMin?: number | string; updateAfterMountMs?: number; onScroll?: React.UIEventHandler; @@ -41,6 +43,7 @@ export const CustomScrollbar: FC = ({ hideHorizontalTrack, hideVerticalTrack, scrollRefCallback, + showScrollIndicators = false, updateAfterMountMs, scrollTop, onScroll, @@ -119,7 +122,9 @@ export const CustomScrollbar: FC = ({ = ({ renderView={renderView} onScroll={onScroll} > - {children} + {showScrollIndicators ? {children} : children} ); }; @@ -188,5 +193,14 @@ const getStyles = (theme: GrafanaTheme2) => { } } `, + // override the scroll container position so that the scroll indicators + // are positioned at the top and bottom correctly. + // react-custom-scrollbars doesn't provide any way for us to hook in nicely, + // so we have to override with !important. feelsbad. + scrollbarWithScrollIndicators: css` + .scrollbar-view { + position: static !important; + } + `, }; }; diff --git a/packages/grafana-ui/src/components/CustomScrollbar/ScrollIndicators.tsx b/packages/grafana-ui/src/components/CustomScrollbar/ScrollIndicators.tsx new file mode 100644 index 00000000000..275250d7a49 --- /dev/null +++ b/packages/grafana-ui/src/components/CustomScrollbar/ScrollIndicators.tsx @@ -0,0 +1,100 @@ +import { css, cx } from '@emotion/css'; +import classNames from 'classnames'; +import React, { FC, useEffect, useRef, useState } from 'react'; + +import { GrafanaTheme2 } from '@grafana/data'; + +import { useStyles2 } from '../../themes'; +import { Icon } from '../Icon/Icon'; + +export const ScrollIndicators: FC = ({ children }) => { + const [showScrollTopIndicator, setShowTopScrollIndicator] = useState(false); + const [showScrollBottomIndicator, setShowBottomScrollIndicator] = useState(false); + const scrollTopMarker = useRef(null); + const scrollBottomMarker = useRef(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 ( + <> +
+ +
+
+
+ {children} +
+
+
+ +
+ + ); +}; + +const getStyles = (theme: GrafanaTheme2) => { + return { + scrollContent: css({ + flex: 1, + position: 'relative', + }), + scrollIndicator: css({ + height: theme.spacing(6), + left: 0, + opacity: 0, + pointerEvents: 'none', + position: 'absolute', + right: 0, + 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, + }), + scrollIcon: css({ + left: '50%', + position: 'absolute', + transform: 'translateX(-50%)', + }), + scrollTopIcon: css({ + top: 0, + }), + scrollBottomIcon: css({ + bottom: 0, + }), + }; +}; diff --git a/public/app/core/components/MegaMenu/NavBarMenu.tsx b/public/app/core/components/MegaMenu/NavBarMenu.tsx index 5eb41abd64f..01e1c105bbc 100644 --- a/public/app/core/components/MegaMenu/NavBarMenu.tsx +++ b/public/app/core/components/MegaMenu/NavBarMenu.tsx @@ -82,7 +82,7 @@ export function NavBarMenu({ activeItem, navItems, searchBarHidden, onClose }: P }} /> diff --git a/public/app/core/components/NavBar/NavBarItemMenu.tsx b/public/app/core/components/NavBar/NavBarItemMenu.tsx index b319e6b0987..7eccd658ec2 100644 --- a/public/app/core/components/NavBar/NavBarItemMenu.tsx +++ b/public/app/core/components/NavBar/NavBarItemMenu.tsx @@ -7,10 +7,9 @@ import { SpectrumMenuProps } from '@react-types/menu'; import React, { ReactElement, useEffect, useRef } from 'react'; import { GrafanaTheme2, NavMenuItemType, NavModelItem } from '@grafana/data'; -import { useTheme2 } from '@grafana/ui'; +import { CustomScrollbar, useTheme2 } from '@grafana/ui'; import { NavBarItemMenuItem } from './NavBarItemMenuItem'; -import { NavBarScrollContainer } from './NavBarScrollContainer'; import { useNavBarItemMenuContext } from './context'; import menuItemTranslations from './navBarItem-translations'; import { getNavModelItemKey } from './utils'; @@ -78,9 +77,9 @@ export function NavBarItemMenu(props: NavBarItemMenuProps): ReactElement | null const contents = [itemComponents, subTitleComponent]; const contentComponent = ( - + {reverseMenuDirection ? contents.reverse() : contents} - + ); const menu = [headerComponent, contentComponent]; diff --git a/public/app/core/components/NavBar/NavBarScrollContainer.tsx b/public/app/core/components/NavBar/NavBarScrollContainer.tsx deleted file mode 100644 index a1d14bb2c9b..00000000000 --- a/public/app/core/components/NavBar/NavBarScrollContainer.tsx +++ /dev/null @@ -1,131 +0,0 @@ -import { css, cx } from '@emotion/css'; -import React, { useEffect, useRef, useState } from 'react'; - -import { GrafanaTheme2 } from '@grafana/data'; -import { CustomScrollbar, Icon, useTheme2 } from '@grafana/ui'; - -export interface Props { - children: React.ReactNode; -} - -export const NavBarScrollContainer = ({ children }: Props) => { - const [showScrollTopIndicator, setShowTopScrollIndicator] = useState(false); - const [showScrollBottomIndicator, setShowBottomScrollIndicator] = useState(false); - const scrollTopMarker = useRef(null); - const scrollBottomMarker = useRef(null); - const theme = useTheme2(); - const styles = getStyles(theme); - - // 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 ( - -
- -
-
-
- {children} -
-
-
- -
-
- ); -}; - -NavBarScrollContainer.displayName = 'NavBarScrollContainer'; - -const getStyles = (theme: GrafanaTheme2) => ({ - 'scrollTopMarker, scrollBottomMarker': css({ - height: theme.spacing(1), - left: 0, - position: 'absolute', - pointerEvents: 'none', - right: 0, - }), - scrollTopMarker: css({ - top: 0, - }), - scrollBottomMarker: css({ - bottom: 0, - }), - scrollContent: css({ - flex: 1, - position: 'relative', - }), - // override the scroll container position so that the scroll indicators - // are positioned at the top and bottom correctly. - // react-custom-scrollbars doesn't provide any way for us to hook in nicely, - // so we have to override with !important. feelsbad. - scrollContainer: css` - .scrollbar-view { - position: static !important; - } - `, - scrollTopIndicator: css({ - background: `linear-gradient(0deg, transparent, ${theme.colors.background.canvas})`, - height: theme.spacing(6), - left: 0, - opacity: 0, - pointerEvents: 'none', - position: 'absolute', - right: 0, - top: 0, - transition: theme.transitions.create('opacity'), - zIndex: theme.zIndex.sidemenu, - }), - scrollBottomIndicator: css({ - background: `linear-gradient(0deg, ${theme.colors.background.canvas}, transparent)`, - bottom: 0, - height: theme.spacing(6), - left: 0, - opacity: 0, - pointerEvents: 'none', - position: 'absolute', - right: 0, - transition: theme.transitions.create('opacity'), - zIndex: theme.zIndex.sidemenu, - }), - scrollIndicatorVisible: css({ - opacity: 1, - }), - scrollTopIcon: css({ - left: '50%', - position: 'absolute', - top: 0, - transform: 'translateX(-50%)', - }), - scrollBottomIcon: css({ - bottom: 0, - left: '50%', - position: 'absolute', - transform: 'translateX(-50%)', - }), -}); diff --git a/public/app/core/components/PageNew/SectionNav.tsx b/public/app/core/components/PageNew/SectionNav.tsx index 0e34bdc1798..7c0b43e9517 100644 --- a/public/app/core/components/PageNew/SectionNav.tsx +++ b/public/app/core/components/PageNew/SectionNav.tsx @@ -23,7 +23,7 @@ export function SectionNav(props: Props) { {main.img && {`logo} {props.model.main.text} - +
{directChildren.map((child, index) => { return (