Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(JumpLinks): support passing a reference to scroll element #9961

Merged
merged 4 commits into from
Feb 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading