diff --git a/packages/react-core/src/components/JumpLinks/JumpLinks.tsx b/packages/react-core/src/components/JumpLinks/JumpLinks.tsx index af0af79a18f..9e3875ffec5 100644 --- a/packages/react-core/src/components/JumpLinks/JumpLinks.tsx +++ b/packages/react-core/src/components/JumpLinks/JumpLinks.tsx @@ -100,8 +100,7 @@ export const JumpLinks: React.FunctionComponent = ({ const [scrollItems, setScrollItems] = useState(hasScrollSpy ? getScrollItems(children, []) : []); const [activeIndex, setActiveIndex] = useState(activeIndexProp); const [isExpanded, setIsExpanded] = useState(isExpandedProp); - // Boolean to disable scroll listener from overriding active state of clicked jumplink - const isLinkClicked = useRef(false); + const ignoreScrollSpyUntil = useRef(null); const navRef = useRef(undefined); let scrollableElement: HTMLElement; @@ -123,11 +122,13 @@ export const JumpLinks: React.FunctionComponent = ({ if (!canUseDOM || !hasScrollSpy || !(scrollableElement instanceof HTMLElement)) { return; } - if (isLinkClicked.current) { - isLinkClicked.current = false; + + const now = performance.now(); + // Ignore scrolls while smooth scrolling is in progress + if (ignoreScrollSpyUntil.current && now < ignoreScrollSpyUntil.current) { return; } - const scrollPosition = Math.ceil(scrollableElement.scrollTop + offset); + window.requestAnimationFrame(() => { let newScrollItems = scrollItems; // Items might have rendered after this component or offsetTop values may need @@ -138,21 +139,8 @@ export const JumpLinks: React.FunctionComponent = ({ newScrollItems = getScrollItems(children, []); setScrollItems(newScrollItems); } - - const scrollElements = newScrollItems - .map((e, index) => ({ - y: e ? e.offsetTop : null, - index - })) - .filter(({ y }) => y !== null) - .sort((e1, e2) => e2.y - e1.y); - for (const { y, index } of scrollElements) { - if (scrollPosition >= y) { - return setActiveIndex(index); - } - } }); - }, [scrollItems, hasScrollSpy, scrollableElement, offset]); + }, [scrollItems, hasScrollSpy, scrollableElement, children]); useEffect(() => { scrollableElement = getScrollableElement(); @@ -179,7 +167,7 @@ export const JumpLinks: React.FunctionComponent = ({ const scrollItem = scrollItems[itemIndex]; return cloneElement(child as React.ReactElement, { onClick(ev: React.MouseEvent) { - isLinkClicked.current = true; + ignoreScrollSpyUntil.current = performance.now() + 1000; // Items might have rendered after this component. Do a quick refresh. let newScrollItems; if (!scrollItem) { @@ -191,31 +179,52 @@ export const JumpLinks: React.FunctionComponent = ({ if (newScrollItem) { // we have to support scrolling to an offset due to sticky sidebar const scrollableElement = getScrollableElement() as HTMLElement; - if (scrollableElement instanceof HTMLElement) { - if (isResponsive(navRef.current)) { - // Remove class immediately so we can get collapsed height - if (navRef.current) { - navRef.current.classList.remove(styles.modifiers.expanded); - } - let stickyParent = navRef.current && navRef.current.parentElement; - while (stickyParent && !stickyParent.classList.contains(sidebarStyles.modifiers.sticky)) { - stickyParent = stickyParent.parentElement; - } - setIsExpanded(false); - if (stickyParent) { - offset += stickyParent.scrollHeight; - } + if (!(scrollableElement instanceof HTMLElement)) { + return; + } + + let effectiveOffset = offset; + + // Remove class immediately so we can get collapsed height + if (isResponsive(navRef.current)) { + navRef.current?.classList.remove(styles.modifiers.expanded); + + let stickyParent = navRef.current?.parentElement; + while (stickyParent && !stickyParent.classList.contains(sidebarStyles.modifiers.sticky)) { + stickyParent = stickyParent.parentElement; + } + setIsExpanded(false); + if (stickyParent) { + effectiveOffset += stickyParent.scrollHeight; } - scrollableElement.scrollTo(0, newScrollItem.offsetTop - offset); } - newScrollItem.focus(); - if (shouldReplaceNavHistory) { - window.history.replaceState('', '', (ev.currentTarget as HTMLAnchorElement).href); - } else { - window.history.pushState('', '', (ev.currentTarget as HTMLAnchorElement).href); + + const href = (ev.currentTarget as HTMLAnchorElement)?.href; + if (href) { + if (shouldReplaceNavHistory) { + window.history.replaceState('', '', href); + } else { + window.history.pushState('', '', href); + } } + ev.preventDefault(); setActiveIndex(itemIndex); + newScrollItem.focus(); + + const targetTop = Math.min( + newScrollItem.offsetTop - effectiveOffset, + scrollableElement.scrollHeight - scrollableElement.clientHeight + ); + + // delays smooth scroll so it actually happens + requestAnimationFrame(() => { + scrollableElement.scrollTo({ + top: targetTop, + left: 0, + behavior: 'smooth' + }); + }); } if (onClickProp) { onClickProp(ev); diff --git a/packages/react-core/src/demos/JumpLinks.md b/packages/react-core/src/demos/JumpLinks.md index 5213798b16e..3bd28c42f76 100644 --- a/packages/react-core/src/demos/JumpLinks.md +++ b/packages/react-core/src/demos/JumpLinks.md @@ -59,8 +59,6 @@ ScrollspyH2 = () => { const jumpLinksHeaderHeight = document.getElementsByClassName('pf-m-sticky')[0].offsetHeight; jumpLinksHeaderHeight && setOffsetHeight(masthead.offsetHeight + jumpLinksHeaderHeight + offsetForPadding); } - - }, [isVertical]); getResizeObserver( @@ -98,12 +96,12 @@ ScrollspyH2 = () => { isVertical={isVertical} isCentered={!isVertical} label="Jump to section" - scrollableSelector=".pf-v6-c-page__main-container" + scrollableSelector="#scrollable-element" offset={offsetHeight} expandable={{ default: isVertical ? 'expandable' : 'nonExpandable', md: 'nonExpandable' }} isExpanded > - {headings.map(i => ( + {headings.map((i) => ( {`Heading ${i}`} @@ -115,7 +113,7 @@ ScrollspyH2 = () => { - {headings.map(i => ( + {headings.map((i) => (

{`Heading ${i}`} @@ -158,4 +156,5 @@ This demo shows how jump links can be used in combination with a drawer. 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" + ```