Skip to content

Commit

Permalink
feat: navscroll refactoring with progressbar
Browse files Browse the repository at this point in the history
  • Loading branch information
astagi authored Dec 9, 2024
1 parent 2dc384e commit d0cfd5b
Show file tree
Hide file tree
Showing 6 changed files with 326 additions and 212 deletions.
8 changes: 4 additions & 4 deletions jest.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -172,13 +172,13 @@ const config = {
transform: {
'.(ts|tsx)$': 'ts-jest',
'.(js|jsx)$': 'babel-jest'
}
},

// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
// transformIgnorePatterns: [
// "/node_modules/",
transformIgnorePatterns: [
"/node_modules/(?!uuid)/"
// "\\.pnp\\.[^\\/]+$"
// ],
],

// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
// unmockedModulePathPatterns: undefined,
Expand Down
4 changes: 4 additions & 0 deletions src/NavScroll/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ export type useNavScrollResult = {
* A function to retrieve the reference of the current active element (only the last element, not the elements hierarchy).
*/
getActiveRef: () => RefObject<Element> | null;
/**
* A list of active ids (the full hierarchy).
*/
percentage: number;
};

// @private
Expand Down
136 changes: 102 additions & 34 deletions src/NavScroll/useNavScroll.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,81 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/*
/*--------------------------------------------------------------------------
* This work derives from the React Use Navscroll library
* Released under the MIT license by Marco Liberati
* Released under the MIT license by Marco Liberati (@dej611)
* Code: https://github.com/dej611/react-use-navscroll
* --------------------------------------------------------------------------
* Parts of this code has been modified using Bootstrap Italia source code
* --------------------------------------------------------------------------
* Bootstrap Italia (https://italia.github.io/bootstrap-italia/)
* Authors: https://github.com/italia/bootstrap-italia/blob/main/AUTHORS
* License: BSD-3-Clause (https://github.com/italia/bootstrap-italia/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/

import { createRef, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { debounce } from './debounce';
import type { TrackedElement, useNavScrollArgs, useNavScrollResult } from './types';
import type { useNavScrollArgs, useNavScrollResult, TrackedElement } from './types';
import { useSizeDetector } from './useSizeDetector';

import { v4 as uuidv4 } from 'uuid';

let ticking: boolean = false;
let callbacks: any[] = [];

class ScrollCallback {
private _callback: any;
id: string;
constructor(id: string, callback: any) {
this.id = id;
this._callback = callback;
}

//Public
dispose() {
removeCallBack(this.id);
}

//Private
_execute(data: any) {
this._callback(data);
}
}

const removeCallBack = (id: string) => {
callbacks = callbacks.filter((cb) => cb.id !== id);
};

const onDocumentScroll = (callback: any) => {
if (typeof document === 'undefined') {
return;
}
if (!callbacks.length) {
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
document.addEventListener('scroll', (evt) => {
if (!ticking) {
window.requestAnimationFrame(() => {
callbacks.forEach((cbObj) => cbObj.cb._execute(evt));
ticking = false;
});
ticking = true;
}
});
}
}

if (typeof callback === 'function') {
const newCb = new ScrollCallback(uuidv4(), callback);
callbacks.push({
id: newCb.id,
cb: newCb
});
return newCb;
}

console.error('[onDocumentScroll] the provided data has to be of type function');
return null;
};

const hasWindow = typeof window !== 'undefined';
const REGISTER_DELAY = 50;

Expand All @@ -34,6 +100,7 @@ export function useNavScroll(args: useNavScrollArgs = {}): useNavScrollResult {
const [counter, setCounter] = useState(0);
const [forceRecompute, setForceRecompute] = useState(false);
const [activeId, updateActiveId] = useState<string | null>(null);
const [percentageValue, setPercentageValue] = useState(0);

const { targetSize, useViewport } = useSizeDetector({
root,
Expand Down Expand Up @@ -63,25 +130,31 @@ export function useNavScroll(args: useNavScrollArgs = {}): useNavScrollResult {
lookup[id] = parent;
}
return lookup;
}, []);
}, [counter]);
const activeIds = useMemo(() => (activeId ? resolveHierarchyIds(activeId, elsLookup) : []), [activeId, elsLookup]);

const activeLookups = useMemo(() => new Set(activeIds), [activeIds]);
useEffect(() => {
if (!hasWindow) {
return;
}
const handleIntersection: IntersectionObserverCallback = (entries) => {
const _onScroll = () => {
let intersectionId = null;
let topMin = Infinity;
entries.forEach((entry) => {
if (entry.isIntersecting) {
if (topMin > entry.boundingClientRect.top) {
topMin = entry.boundingClientRect.top;
intersectionId = entry.target.id;
}
for (let k = 0; k < els.current.length; k++) {
const entry = els.current[k].ref.current;
const min = entry?.getBoundingClientRect().top ? entry?.getBoundingClientRect().top : 0;
if (!min) {
break;
}
});
if (min > 0 && k > 0) {
const totEls =
root?.previousSibling?.firstChild?.parentNode?.querySelectorAll('.it-navscroll-wrapper .nav-link').length ||
1;
setPercentageValue((k / (totEls / 2)) * 100);
intersectionId = els.current[k - 1].ref.current?.id;
break;
}
}
if (intersectionId != null) {
updateActiveId(intersectionId);
if (onChange) {
Expand All @@ -94,21 +167,11 @@ export function useNavScroll(args: useNavScrollArgs = {}): useNavScrollResult {
}
};

const observer = new IntersectionObserver(handleIntersection, observerOptions);

els.current.forEach(({ ref }) => {
if (ref && ref.current) {
observer.observe(ref.current);
}
});
onDocumentScroll(_onScroll);

if (forceRecompute) {
handleIntersection(observer.takeRecords(), observer);
setForceRecompute(false);
}
return () => {
observer.disconnect();
};
setTimeout(() => {
_onScroll();
}, 300);
}, [
activeIds,
updateActiveId,
Expand All @@ -131,35 +194,40 @@ export function useNavScroll(args: useNavScrollArgs = {}): useNavScrollResult {
);

const register = useCallback(
(id: string, options = {}) => {
(id: string, options: any = {}) => {
if (!hasWindow) {
return { id, ref: null };
}
const alreadyRegistered = id in elsLookup;
const entry = (alreadyRegistered ? els.current.find(({ id: existingId }) => existingId === id) : options) as any;
const entry = alreadyRegistered ? els.current.find(({ id: existingId }) => existingId === id) : options;
const ref = (entry && entry.ref) || createRef();

if (!alreadyRegistered) {
els.current = [...els.current, { id, ref, parent: (options as any).parent }];
els.current = [...els.current, { id, ref, parent: options.parent }];
refresh();
}
return { id, ref };
},
[elsLookup, refresh]
[counter]
);

const unregister = useCallback((idToUnregister: string) => {
els.current = els.current.filter(({ id }) => id !== idToUnregister);
}, []);
const unregister = useCallback(
(idToUnregister: string) => {
els.current = els.current.filter(({ id }) => id !== idToUnregister);
},
[counter]
);

const isActive = useCallback((id: string) => activeLookups.has(id), [activeLookups]);
const percentage = useMemo(() => percentageValue, [percentageValue]);

const getActiveRef = useCallback(() => {
const entry = els.current.find(({ id }) => id === activeId);
return entry ? entry.ref : null;
}, [activeId]);

return {
percentage,
register,
unregister,
activeIds,
Expand Down
9 changes: 3 additions & 6 deletions stories/Components/LinkList/LinkList.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -243,12 +243,9 @@ const CollapseExampleHooks = () => {
toggleCollapse3(!collapseOpen3);
};

const expanded = {
"aria-expanded": true,
};
return (
<LinkList>
<LinkListItem bold large className="icon-right" onClick={onToggle1} {...(collapseOpen1 ? expanded : {})}>
<LinkListItem bold large className="icon-right" onClick={onToggle1} aria-expanded={collapseOpen1}>
<LinkListItem.TitleIconWrapper>
<span>Link list 1 </span>
<Icon className="right" color="primary" icon="it-expand" aria-hidden />
Expand All @@ -267,7 +264,7 @@ const CollapseExampleHooks = () => {
</LinkListItem>
</LinkList>
</Collapse>
<LinkListItem bold large className="icon-right" onClick={onToggle2} {...(collapseOpen2 ? expanded : {})}>
<LinkListItem bold large className="icon-right" onClick={onToggle2} aria-expanded={collapseOpen2}>
<LinkListItem.TitleIconWrapper>
<span>Link list 2 </span>
<Icon className="right" color="primary" icon="it-expand" aria-hidden />
Expand All @@ -286,7 +283,7 @@ const CollapseExampleHooks = () => {
</LinkListItem>
</LinkList>
</Collapse>
<LinkListItem bold large className="icon-right" onClick={onToggle3} {...(collapseOpen3 ? expanded : {})}>
<LinkListItem bold large className="icon-right" onClick={onToggle3} aria-expanded={collapseOpen3}>
<LinkListItem.TitleIconWrapper>
<span>Link list 3 </span>
<Icon className="right" color="primary" icon="it-expand" aria-hidden />
Expand Down
Loading

0 comments on commit d0cfd5b

Please sign in to comment.