Skip to content

Commit ba8605f

Browse files
committed
feat(ktl-4093): implement smooth scrolling to anchor elements on masonry layout ready
1 parent f4b93b2 commit ba8605f

File tree

3 files changed

+65
-2
lines changed

3 files changed

+65
-2
lines changed

blocks/case-studies/grid/case-studies-grid.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { FC } from 'react';
1+
import { FC, useState } from 'react';
22
import cn from 'classnames';
33
import { CaseStudyCard, CaseStudyCardProps } from '../card/case-studies-card';
44
import { useFilteredCases } from '../filter/use-filtered-cases';
@@ -7,6 +7,7 @@ import { EmptyState } from '../../../components/empty-state/empry-state';
77
import { ThemeProvider } from '@rescui/ui-contexts';
88

99
import styles from './case-studies-grid.module.css';
10+
import { useDeferredAnchorScroll } from '../../../hooks/useDeferredAnchorScroll';
1011

1112
export type CaseStudiesGridProps = {
1213
className?: string;
@@ -23,6 +24,9 @@ export const CaseStudiesSection: FC<CaseStudiesGridProps> = ({ className, ...pro
2324
export const CaseStudiesGrid: FC<CaseStudiesGridProps> = ({ className, mode, showCopyLinkButton }) => {
2425
const cases = useFilteredCases();
2526
const theme = 'light';
27+
const [isLayoutReady, setIsLayoutReady] = useState(false);
28+
29+
useDeferredAnchorScroll(isLayoutReady);
2630

2731
return (
2832
<ThemeProvider theme={theme}>
@@ -38,6 +42,7 @@ export const CaseStudiesGrid: FC<CaseStudiesGridProps> = ({ className, mode, sho
3842
<CaseStudyCard mode={mode} showCopyLinkButton={showCopyLinkButton} {...caseItem} />
3943
)}
4044
getKey={(caseItem) => caseItem.id}
45+
onLayoutReady={() => setIsLayoutReady(true)}
4146
/>
4247
)}
4348
</div>

components/masonry-grid/masonry-grid.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export interface MasonryGridProps<T> {
1212
columnClassName?: string;
1313
itemClassName?: string;
1414
mobileBreakpoint?: number;
15+
onLayoutReady?: () => void;
1516
}
1617

1718
export function MasonryGrid<T>({
@@ -24,6 +25,7 @@ export function MasonryGrid<T>({
2425
columnClassName,
2526
itemClassName,
2627
mobileBreakpoint = 808,
28+
onLayoutReady
2729
}: MasonryGridProps<T>) {
2830
const [isMobile, setIsMobile] = useState(false);
2931

@@ -107,6 +109,7 @@ export function MasonryGrid<T>({
107109
});
108110

109111
setGreedyColumns(cols);
112+
setTimeout(onLayoutReady, 0);
110113
}, 0);
111114

112115
return () => window.clearTimeout(id);
@@ -145,7 +148,6 @@ export function MasonryGrid<T>({
145148
key={getKey(item, originalIndex)}
146149
className={cn(styles.item, itemClassName)}
147150
>
148-
{/*<div style={{color: 'white'}}>{originalIndex}</div>*/}
149151
{renderItem(item, originalIndex)}
150152
</div>
151153
);

hooks/useDeferredAnchorScroll.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { useEffect, useRef } from 'react';
2+
3+
// Strip the hash early (module-level, runs before any render/scroll)
4+
// so the browser doesn't attempt its native scroll-to-anchor.
5+
const savedHash =
6+
typeof window !== 'undefined' && window.location.hash ? window.location.hash : '';
7+
if (savedHash) {
8+
history.replaceState(null, '', window.location.pathname + window.location.search);
9+
}
10+
11+
/**
12+
* Defers anchor scrolling until layout is ready.
13+
*
14+
* IMPORTANT: Importing this module strips the URL hash immediately
15+
* to prevent the browser's native scroll-to-anchor behavior.
16+
*/
17+
export function useDeferredAnchorScroll(isLayoutReady: boolean) {
18+
const hasScrolledToAnchor = useRef(false);
19+
20+
useEffect(() => {
21+
if (!isLayoutReady) return;
22+
if (hasScrolledToAnchor.current) return;
23+
if (!savedHash) return;
24+
25+
// Restore the hash silently (without triggering the browser scroll)
26+
history.replaceState(null, '', savedHash);
27+
markTargetElement();
28+
scrollToTargetElement();
29+
hasScrolledToAnchor.current = true;
30+
}, [isLayoutReady]);
31+
}
32+
33+
function markTargetElement() {
34+
const hash = window.location.hash;
35+
if (!hash) return;
36+
37+
const elementId = hash.substring(1);
38+
const targetElement = document.getElementById(elementId);
39+
if (targetElement) {
40+
targetElement.setAttribute('data-target', 'true');
41+
}
42+
}
43+
44+
function scrollToTargetElement() {
45+
const hash = window.location.hash;
46+
if (!hash) return;
47+
48+
const elementId = hash.substring(1);
49+
const targetElement = document.getElementById(elementId);
50+
if (targetElement) {
51+
targetElement.scrollIntoView({
52+
behavior: 'smooth',
53+
block: 'start'
54+
});
55+
}
56+
}

0 commit comments

Comments
 (0)