Skip to content

Commit a5a180c

Browse files
feat(JumpLinks): support passing a reference to scroll element (#9961)
* 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 <cvogt@redhat.com> * docs(JumpLinks): mention scrollableRef as an option --------- Co-authored-by: Christian Vogt <cvogt@redhat.com>
1 parent 2970f8a commit a5a180c

File tree

3 files changed

+50
-26
lines changed

3 files changed

+50
-26
lines changed

packages/react-core/src/components/JumpLinks/JumpLinks.tsx

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ export interface JumpLinksProps extends Omit<React.HTMLProps<HTMLElement>, 'labe
2121
alwaysShowLabel?: boolean;
2222
/** Adds an accessible label to the internal nav element. Defaults to the value of the label prop. */
2323
'aria-label'?: string;
24-
/** Selector for the scrollable element to spy on. Not passing a selector disables spying. */
24+
/** Reference to the scrollable element to spy on. Takes precedence over scrollableSelector. Not passing a scrollableRef or scrollableSelector disables spying. */
25+
scrollableRef?: HTMLElement | (() => HTMLElement) | React.RefObject<HTMLElement>;
26+
/** Selector for the scrollable element to spy on. Not passing a scrollableSelector or scrollableRef disables spying. */
2527
scrollableSelector?: string;
2628
/** The index of the child Jump link to make active. */
2729
activeIndex?: number;
@@ -81,6 +83,7 @@ export const JumpLinks: React.FunctionComponent<JumpLinksProps> = ({
8183
children,
8284
label,
8385
'aria-label': ariaLabel = typeof label === 'string' ? label : null,
86+
scrollableRef,
8487
scrollableSelector,
8588
activeIndex: activeIndexProp = 0,
8689
offset = 0,
@@ -91,18 +94,29 @@ export const JumpLinks: React.FunctionComponent<JumpLinksProps> = ({
9194
className,
9295
...props
9396
}: JumpLinksProps) => {
94-
const hasScrollSpy = Boolean(scrollableSelector);
97+
const hasScrollSpy = Boolean(scrollableRef || scrollableSelector);
9598
const [scrollItems, setScrollItems] = React.useState(hasScrollSpy ? getScrollItems(children, []) : []);
9699
const [activeIndex, setActiveIndex] = React.useState(activeIndexProp);
97100
const [isExpanded, setIsExpanded] = React.useState(isExpandedProp);
98101
// Boolean to disable scroll listener from overriding active state of clicked jumplink
99102
const isLinkClicked = React.useRef(false);
100-
// Allow expanding to be controlled for a niche use case
101-
React.useEffect(() => setIsExpanded(isExpandedProp), [isExpandedProp]);
102103
const navRef = React.useRef<HTMLElement>();
103104

104105
let scrollableElement: HTMLElement;
105106

107+
const getScrollableElement = () => {
108+
if (scrollableRef) {
109+
if (scrollableRef instanceof HTMLElement) {
110+
return scrollableRef;
111+
} else if (typeof scrollableRef === 'function') {
112+
return scrollableRef();
113+
}
114+
return (scrollableRef as React.RefObject<HTMLElement>).current;
115+
} else if (scrollableSelector) {
116+
return document.querySelector(scrollableSelector) as HTMLElement;
117+
}
118+
};
119+
106120
const scrollSpy = React.useCallback(() => {
107121
if (!canUseDOM || !hasScrollSpy || !(scrollableElement instanceof HTMLElement)) {
108122
return;
@@ -139,14 +153,14 @@ export const JumpLinks: React.FunctionComponent<JumpLinksProps> = ({
139153
}, [scrollItems, hasScrollSpy, scrollableElement, offset]);
140154

141155
React.useEffect(() => {
142-
scrollableElement = document.querySelector(scrollableSelector) as HTMLElement;
156+
scrollableElement = getScrollableElement();
143157
if (!(scrollableElement instanceof HTMLElement)) {
144158
return;
145159
}
146160
scrollableElement.addEventListener('scroll', scrollSpy);
147161

148162
return () => scrollableElement.removeEventListener('scroll', scrollSpy);
149-
}, [scrollableSelector, scrollSpy]);
163+
}, [scrollableElement, scrollSpy, getScrollableElement]);
150164

151165
React.useEffect(() => {
152166
scrollSpy();
@@ -174,7 +188,7 @@ export const JumpLinks: React.FunctionComponent<JumpLinksProps> = ({
174188

175189
if (newScrollItem) {
176190
// we have to support scrolling to an offset due to sticky sidebar
177-
const scrollableElement = document.querySelector(scrollableSelector) as HTMLElement;
191+
const scrollableElement = getScrollableElement() as HTMLElement;
178192
if (scrollableElement instanceof HTMLElement) {
179193
if (isResponsive(navRef.current)) {
180194
// Remove class immediately so we can get collapsed height

packages/react-core/src/demos/JumpLinks.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@ import mastheadStyles from '@patternfly/react-styles/css/components/Masthead/mas
99

1010
JumpLinks has a scrollspy built-in to make your implementation easier. When implementing JumpLinks be sure to:
1111

12-
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).
12+
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).
1313
- If you add `hasOverflowScroll` to a Page sub-component you should also add a relevant aria-label to that component as well.
14-
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.
14+
2. Provide a reference to the scrollable element to `scrollableRef` prop or a CSS selector of the scrollable element to `scrollableSelector` prop.
15+
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.
1516

1617
### Scrollspy with subsections
1718

@@ -141,7 +142,7 @@ ScrollspyH2 = () => {
141142

142143
This demo shows how jump links can be used in combination with a drawer.
143144

144-
The `scrollableSelector` prop passed to the jump links component is an `id` that was placed on the `DrawerContent` component.
145+
This demo uses a `scrollableRef` prop on the JumpLinks component, which is a React ref to the `DrawerContent` component.
145146

146147
```js isFullscreen file="./examples/JumpLinks/JumpLinksWithDrawer.js"
147148
```

packages/react-core/src/demos/examples/JumpLinks/JumpLinksWithDrawer.js

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,33 @@ import {
1717
SidebarContent,
1818
SidebarPanel,
1919
TextContent,
20-
getResizeObserver
20+
getResizeObserver,
21+
DrawerContext
2122
} from '@patternfly/react-core';
2223
import { DashboardWrapper } from '@patternfly/react-core/src/demos/DashboardWrapper';
2324
import mastheadStyles from '@patternfly/react-styles/css/components/Masthead/masthead';
2425

26+
const JumpLinksWrapper = ({ offsetHeight, headings }) => {
27+
const { drawerContentRef } = React.useContext(DrawerContext);
28+
29+
return (
30+
<JumpLinks
31+
isVertical={true}
32+
label="Jump to section"
33+
scrollableRef={drawerContentRef}
34+
offset={offsetHeight}
35+
expandable={{ default: 'expandable', md: 'nonExpandable' }}
36+
>
37+
{headings.map((heading) => (
38+
<JumpLinksItem key={heading} href={`#jump-links-drawer-jump-links-${heading.toLowerCase()}`}>
39+
{`${heading} section`}
40+
<JumpLinksList></JumpLinksList>
41+
</JumpLinksItem>
42+
))}
43+
</JumpLinks>
44+
);
45+
};
46+
2547
export const JumpLinksWithDrawer = () => {
2648
const headings = ['First', 'Second', 'Third', 'Fourth', 'Fifth'];
2749

@@ -66,25 +88,12 @@ export const JumpLinksWithDrawer = () => {
6688
return (
6789
<DashboardWrapper breadcrumb={null} mainContainerId="scrollable-element">
6890
<Drawer isExpanded={isExpanded}>
69-
<DrawerContent panelContent={panelContent} id="jump-links-drawer-drawer-scrollable-container">
91+
<DrawerContent panelContent={panelContent}>
7092
<DrawerContentBody>
7193
<Sidebar>
7294
<SidebarPanel variant="sticky">
7395
<PageSection variant={PageSectionVariants.light}>
74-
<JumpLinks
75-
isVertical={true}
76-
label="Jump to section"
77-
scrollableSelector="#jump-links-drawer-drawer-scrollable-container"
78-
offset={offsetHeight}
79-
expandable={{ default: 'expandable', md: 'nonExpandable' }}
80-
>
81-
{headings.map((heading) => (
82-
<JumpLinksItem key={heading} href={`#jump-links-drawer-jump-links-${heading.toLowerCase()}`}>
83-
{`${heading} section`}
84-
<JumpLinksList></JumpLinksList>
85-
</JumpLinksItem>
86-
))}
87-
</JumpLinks>
96+
<JumpLinksWrapper offsetHeight={offsetHeight} headings={headings} />
8897
</PageSection>
8998
</SidebarPanel>
9099
<SidebarContent>

0 commit comments

Comments
 (0)