Skip to content

Commit 4073625

Browse files
authored
Nojira | @ericbakenhus | Add jumplink hook and wrapper (#696)
* Add jumplink hook and wrapper * Use new fangled useEffectEvent * Use new fangled useEffectEvent * Wait * Wait * Oops * Handle tabindex * Validate hash
1 parent 3232937 commit 4073625

File tree

3 files changed

+63
-16
lines changed

3 files changed

+63
-16
lines changed

app/layout.tsx

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { getGlobalAlerts, getSearchConfigBlok } from '@/utilities/data';
1010
import { SearchModalProvider } from '@/components/Search/Modal/SearchModalContext';
1111
import { MotionProvider } from '@/components/MotionProvider';
1212
import { GlobalAlertsProvider } from '@/components/Alert';
13+
import { HashLinkWrapper } from '@/components/HashLinkWrapper';
1314
import { getStoryblokClient } from '@/utilities/storyblok';
1415

1516
// https://docs.fontawesome.com/web/use-with/react/use-with#getting-font-awesome-css-to-work
@@ -53,22 +54,24 @@ const RootLayout = async ({ children }: LayoutProps) => {
5354
<GlobalAlertsProvider globalAlerts={globalAlerts}>
5455
<SearchModalProvider searchConfig={searchConfig}>
5556
<MotionProvider>
56-
<html
57-
lang="en"
58-
className={cnb(
59-
source_sans.variable,
60-
source_serif.variable,
61-
stanford.variable,
62-
)}
63-
>
64-
<GTAG />
65-
{/* Absolutely necessary to have a body tag here, otherwise your components won't get any interactivity */}
66-
<body>
67-
<FlexBox justifyContent="between" direction="col" className="min-h-screen relative">
68-
{children}
69-
</FlexBox>
70-
</body>
71-
</html>
57+
<HashLinkWrapper>
58+
<html
59+
lang="en"
60+
className={cnb(
61+
source_sans.variable,
62+
source_serif.variable,
63+
stanford.variable,
64+
)}
65+
>
66+
<GTAG />
67+
{/* Absolutely necessary to have a body tag here, otherwise your components won't get any interactivity */}
68+
<body>
69+
<FlexBox justifyContent="between" direction="col" className="min-h-screen relative">
70+
{children}
71+
</FlexBox>
72+
</body>
73+
</html>
74+
</HashLinkWrapper>
7275
</MotionProvider>
7376
</SearchModalProvider>
7477
</GlobalAlertsProvider>

components/HashLinkWrapper.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
'use client';
2+
3+
import { useHashLink } from '@/hooks/useHashLink';
4+
5+
interface HashLinkWrapperProps {
6+
children: React.ReactNode;
7+
}
8+
9+
export const HashLinkWrapper = ({ children }: HashLinkWrapperProps) => {
10+
useHashLink();
11+
12+
return children;
13+
};

hooks/useHashLink.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { useEffect, useEffectEvent } from 'react';
2+
3+
export const useHashLink = () => {
4+
const onReady = useEffectEvent(() => {
5+
const hash = window?.location?.hash?.replace('#', '');
6+
if (!hash) return;
7+
8+
// What we'd expect from Storyblok or developers
9+
const isHashValidId = /^[a-zA-Z][a-zA-Z0-9_-]*$/.test(hash);
10+
if (!isHashValidId) return;
11+
12+
const el = document?.getElementById(hash);
13+
if (!el) return;
14+
15+
const prevTabIndex = el.getAttribute('tabindex');
16+
el.setAttribute('tabindex', '-1');
17+
el.scrollIntoView({ behavior: 'instant' });
18+
el.focus({ preventScroll: true });
19+
if (prevTabIndex) {
20+
el.setAttribute('tabindex', prevTabIndex);
21+
} else {
22+
el.removeAttribute('tabindex');
23+
}
24+
});
25+
26+
useEffect(() => {
27+
setTimeout(() => {
28+
onReady();
29+
}, 100);
30+
}, []);
31+
};

0 commit comments

Comments
 (0)