Skip to content

Commit

Permalink
feat(JumpLinks): support passing a reference to scroll element (#9961)
Browse files Browse the repository at this point in the history
* feat(JumpLinks): support passing a reference to scroll HTMLElement

* docs(JumpLinks): update demo to show example with scrollableRef

* refactor(JumpLinks): getScrollableElement check if HTMLElement first

Co-authored-by: Christian Vogt <[email protected]>

* docs(JumpLinks): mention scrollableRef as an option

---------

Co-authored-by: Christian Vogt <[email protected]>
  • Loading branch information
adamviktora and christianvogt authored Feb 19, 2024
1 parent 2970f8a commit a5a180c
Show file tree
Hide file tree
Showing 3 changed files with 50 additions and 26 deletions.
28 changes: 21 additions & 7 deletions packages/react-core/src/components/JumpLinks/JumpLinks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ export interface JumpLinksProps extends Omit<React.HTMLProps<HTMLElement>, '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<HTMLElement>;
/** 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;
Expand Down Expand Up @@ -81,6 +83,7 @@ export const JumpLinks: React.FunctionComponent<JumpLinksProps> = ({
children,
label,
'aria-label': ariaLabel = typeof label === 'string' ? label : null,
scrollableRef,
scrollableSelector,
activeIndex: activeIndexProp = 0,
offset = 0,
Expand All @@ -91,18 +94,29 @@ export const JumpLinks: React.FunctionComponent<JumpLinksProps> = ({
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<HTMLElement>();

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<HTMLElement>).current;
} else if (scrollableSelector) {
return document.querySelector(scrollableSelector) as HTMLElement;
}
};

const scrollSpy = React.useCallback(() => {
if (!canUseDOM || !hasScrollSpy || !(scrollableElement instanceof HTMLElement)) {
return;
Expand Down Expand Up @@ -139,14 +153,14 @@ export const JumpLinks: React.FunctionComponent<JumpLinksProps> = ({
}, [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();
Expand Down Expand Up @@ -174,7 +188,7 @@ export const JumpLinks: React.FunctionComponent<JumpLinksProps> = ({

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
Expand Down
7 changes: 4 additions & 3 deletions packages/react-core/src/demos/JumpLinks.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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"
```
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<JumpLinks
isVertical={true}
label="Jump to section"
scrollableRef={drawerContentRef}
offset={offsetHeight}
expandable={{ default: 'expandable', md: 'nonExpandable' }}
>
{headings.map((heading) => (
<JumpLinksItem key={heading} href={`#jump-links-drawer-jump-links-${heading.toLowerCase()}`}>
{`${heading} section`}
<JumpLinksList></JumpLinksList>
</JumpLinksItem>
))}
</JumpLinks>
);
};

export const JumpLinksWithDrawer = () => {
const headings = ['First', 'Second', 'Third', 'Fourth', 'Fifth'];

Expand Down Expand Up @@ -66,25 +88,12 @@ export const JumpLinksWithDrawer = () => {
return (
<DashboardWrapper breadcrumb={null} mainContainerId="scrollable-element">
<Drawer isExpanded={isExpanded}>
<DrawerContent panelContent={panelContent} id="jump-links-drawer-drawer-scrollable-container">
<DrawerContent panelContent={panelContent}>
<DrawerContentBody>
<Sidebar>
<SidebarPanel variant="sticky">
<PageSection variant={PageSectionVariants.light}>
<JumpLinks
isVertical={true}
label="Jump to section"
scrollableSelector="#jump-links-drawer-drawer-scrollable-container"
offset={offsetHeight}
expandable={{ default: 'expandable', md: 'nonExpandable' }}
>
{headings.map((heading) => (
<JumpLinksItem key={heading} href={`#jump-links-drawer-jump-links-${heading.toLowerCase()}`}>
{`${heading} section`}
<JumpLinksList></JumpLinksList>
</JumpLinksItem>
))}
</JumpLinks>
<JumpLinksWrapper offsetHeight={offsetHeight} headings={headings} />
</PageSection>
</SidebarPanel>
<SidebarContent>
Expand Down

0 comments on commit a5a180c

Please sign in to comment.