diff --git a/packages/react-core/src/components/JumpLinks/JumpLinks.tsx b/packages/react-core/src/components/JumpLinks/JumpLinks.tsx index 345dcb2ef87..5f75d72b89a 100644 --- a/packages/react-core/src/components/JumpLinks/JumpLinks.tsx +++ b/packages/react-core/src/components/JumpLinks/JumpLinks.tsx @@ -21,7 +21,9 @@ export interface JumpLinksProps extends Omit, 'labe alwaysShowLabel?: boolean; /** Adds an accessible label to the internal nav element. Defaults to the value of the label prop. */ 'aria-label'?: string; - /** Selector for the scrollable element to spy on. Not passing a selector disables spying. */ + /** Reference to the scrollable element to spy on. Takes precedence over scrollableSelector. Not passing a scrollableRef or scrollableSelector disables spying. */ + scrollableRef?: HTMLElement | (() => HTMLElement) | React.RefObject; + /** Selector for the scrollable element to spy on. Not passing a scrollableSelector or scrollableRef disables spying. */ scrollableSelector?: string; /** The index of the child Jump link to make active. */ activeIndex?: number; @@ -81,6 +83,7 @@ export const JumpLinks: React.FunctionComponent = ({ children, label, 'aria-label': ariaLabel = typeof label === 'string' ? label : null, + scrollableRef, scrollableSelector, activeIndex: activeIndexProp = 0, offset = 0, @@ -91,18 +94,29 @@ export const JumpLinks: React.FunctionComponent = ({ className, ...props }: JumpLinksProps) => { - const hasScrollSpy = Boolean(scrollableSelector); + const hasScrollSpy = Boolean(scrollableRef || scrollableSelector); const [scrollItems, setScrollItems] = React.useState(hasScrollSpy ? getScrollItems(children, []) : []); const [activeIndex, setActiveIndex] = React.useState(activeIndexProp); const [isExpanded, setIsExpanded] = React.useState(isExpandedProp); // Boolean to disable scroll listener from overriding active state of clicked jumplink const isLinkClicked = React.useRef(false); - // Allow expanding to be controlled for a niche use case - React.useEffect(() => setIsExpanded(isExpandedProp), [isExpandedProp]); const navRef = React.useRef(); let scrollableElement: HTMLElement; + const getScrollableElement = () => { + if (scrollableRef) { + if (scrollableRef instanceof HTMLElement) { + return scrollableRef; + } else if (typeof scrollableRef === 'function') { + return scrollableRef(); + } + return (scrollableRef as React.RefObject).current; + } else if (scrollableSelector) { + return document.querySelector(scrollableSelector) as HTMLElement; + } + }; + const scrollSpy = React.useCallback(() => { if (!canUseDOM || !hasScrollSpy || !(scrollableElement instanceof HTMLElement)) { return; @@ -139,14 +153,14 @@ export const JumpLinks: React.FunctionComponent = ({ }, [scrollItems, hasScrollSpy, scrollableElement, offset]); React.useEffect(() => { - scrollableElement = document.querySelector(scrollableSelector) as HTMLElement; + scrollableElement = getScrollableElement(); if (!(scrollableElement instanceof HTMLElement)) { return; } scrollableElement.addEventListener('scroll', scrollSpy); return () => scrollableElement.removeEventListener('scroll', scrollSpy); - }, [scrollableSelector, scrollSpy]); + }, [scrollableElement, scrollSpy, getScrollableElement]); React.useEffect(() => { scrollSpy(); @@ -174,7 +188,7 @@ export const JumpLinks: React.FunctionComponent = ({ if (newScrollItem) { // we have to support scrolling to an offset due to sticky sidebar - const scrollableElement = document.querySelector(scrollableSelector) as HTMLElement; + const scrollableElement = getScrollableElement() as HTMLElement; if (scrollableElement instanceof HTMLElement) { if (isResponsive(navRef.current)) { // Remove class immediately so we can get collapsed height diff --git a/packages/react-core/src/demos/JumpLinks.md b/packages/react-core/src/demos/JumpLinks.md index 2e20acb7e55..0753dd8c8ea 100644 --- a/packages/react-core/src/demos/JumpLinks.md +++ b/packages/react-core/src/demos/JumpLinks.md @@ -9,9 +9,10 @@ import mastheadStyles from '@patternfly/react-styles/css/components/Masthead/mas JumpLinks has a scrollspy built-in to make your implementation easier. When implementing JumpLinks be sure to: -1. Find the correct `scrollableSelector` for your page via [Firefox's debugging scrollable overflow](https://developer.mozilla.org/en-US/docs/Tools/Page_Inspector/How_to/Debug_Scrollable_Overflow) or by adding `hasOverflowScroll` to a [PageSection](/components/page#pagesection) or [PageGroup](/components/page#pagegroup). +1. Find the correct scrollable element for your page via [Firefox's debugging scrollable overflow](https://developer.mozilla.org/en-US/docs/Tools/Page_Inspector/How_to/Debug_Scrollable_Overflow) or by adding `hasOverflowScroll` to a [PageSection](/components/page#pagesection) or [PageGroup](/components/page#pagegroup). - If you add `hasOverflowScroll` to a Page sub-component you should also add a relevant aria-label to that component as well. -2. Provide `href`s to your JumpLinksItems which match the `id` of elements you want to spy on. If you wish to scroll to a different item than you're linking to use the `node` prop. +2. Provide a reference to the scrollable element to `scrollableRef` prop or a CSS selector of the scrollable element to `scrollableSelector` prop. +3. Provide `href`s to your JumpLinksItems which match the `id` of elements you want to spy on. If you wish to scroll to a different item than you're linking to use the `node` prop. ### Scrollspy with subsections @@ -141,7 +142,7 @@ ScrollspyH2 = () => { This demo shows how jump links can be used in combination with a drawer. -The `scrollableSelector` prop passed to the jump links component is an `id` that was placed on the `DrawerContent` component. +This demo uses a `scrollableRef` prop on the JumpLinks component, which is a React ref to the `DrawerContent` component. ```js isFullscreen file="./examples/JumpLinks/JumpLinksWithDrawer.js" ``` diff --git a/packages/react-core/src/demos/examples/JumpLinks/JumpLinksWithDrawer.js b/packages/react-core/src/demos/examples/JumpLinks/JumpLinksWithDrawer.js index 14884c5e051..beb980e9b70 100644 --- a/packages/react-core/src/demos/examples/JumpLinks/JumpLinksWithDrawer.js +++ b/packages/react-core/src/demos/examples/JumpLinks/JumpLinksWithDrawer.js @@ -17,11 +17,33 @@ import { SidebarContent, SidebarPanel, TextContent, - getResizeObserver + getResizeObserver, + DrawerContext } from '@patternfly/react-core'; import { DashboardWrapper } from '@patternfly/react-core/src/demos/DashboardWrapper'; import mastheadStyles from '@patternfly/react-styles/css/components/Masthead/masthead'; +const JumpLinksWrapper = ({ offsetHeight, headings }) => { + const { drawerContentRef } = React.useContext(DrawerContext); + + return ( + + {headings.map((heading) => ( + + {`${heading} section`} + + + ))} + + ); +}; + export const JumpLinksWithDrawer = () => { const headings = ['First', 'Second', 'Third', 'Fourth', 'Fifth']; @@ -66,25 +88,12 @@ export const JumpLinksWithDrawer = () => { return ( - + - - {headings.map((heading) => ( - - {`${heading} section`} - - - ))} - +