From 19fec8a531329156f51209c0b7f1b180da89aab1 Mon Sep 17 00:00:00 2001 From: Svyatoslav Date: Mon, 4 Mar 2024 10:15:05 +0300 Subject: [PATCH] feat(useScrollSpy): add new hook --- packages/react/src/hooks/index.ts | 1 + .../react/src/hooks/useScrollSpy/index.ts | 1 + .../hooks/useScrollSpy/useScrollSpy.api.mdx | 26 +++++++ .../useScrollSpy/useScrollSpy.stories.tsx | 74 +++++++++++++++++++ .../src/hooks/useScrollSpy/useScrollSpy.ts | 38 ++++++++++ 5 files changed, 140 insertions(+) create mode 100644 packages/react/src/hooks/useScrollSpy/index.ts create mode 100644 packages/react/src/hooks/useScrollSpy/useScrollSpy.api.mdx create mode 100644 packages/react/src/hooks/useScrollSpy/useScrollSpy.stories.tsx create mode 100644 packages/react/src/hooks/useScrollSpy/useScrollSpy.ts diff --git a/packages/react/src/hooks/index.ts b/packages/react/src/hooks/index.ts index b04e2456a..9b026ea79 100644 --- a/packages/react/src/hooks/index.ts +++ b/packages/react/src/hooks/index.ts @@ -24,6 +24,7 @@ export { usePreviousValue } from './usePreviousValue'; export { useResizeObserver } from './useResizeObserver'; export { useScrollDirection } from './useScrollDirection'; export { useScrollLock } from './useScrollLock'; +export { useScrollSpy } from './useScrollSpy'; export { useScrollSync } from './useScrollSync'; export { useSessionStorage } from './useSessionStorage'; export { useSticky } from './useSticky'; diff --git a/packages/react/src/hooks/useScrollSpy/index.ts b/packages/react/src/hooks/useScrollSpy/index.ts new file mode 100644 index 000000000..8c7b41831 --- /dev/null +++ b/packages/react/src/hooks/useScrollSpy/index.ts @@ -0,0 +1 @@ +export { useScrollSpy } from './useScrollSpy'; diff --git a/packages/react/src/hooks/useScrollSpy/useScrollSpy.api.mdx b/packages/react/src/hooks/useScrollSpy/useScrollSpy.api.mdx new file mode 100644 index 000000000..6a291ef2a --- /dev/null +++ b/packages/react/src/hooks/useScrollSpy/useScrollSpy.api.mdx @@ -0,0 +1,26 @@ +import { Meta } from '@storybook/addon-docs'; +import LinkTo from '@storybook/addon-links/react'; +import { TableFunction } from '~storybook/components/TableFunction'; + + + +# useScrollSpy API + +```js +import { useScrollSpy } from '@elonkit/react'; +``` + + + +
+
+ +## Demos + + diff --git a/packages/react/src/hooks/useScrollSpy/useScrollSpy.stories.tsx b/packages/react/src/hooks/useScrollSpy/useScrollSpy.stories.tsx new file mode 100644 index 000000000..ee6b2d721 --- /dev/null +++ b/packages/react/src/hooks/useScrollSpy/useScrollSpy.stories.tsx @@ -0,0 +1,74 @@ +import { useRef } from 'react'; + +import { Meta, StoryObj } from '@storybook/react'; + +import { useScrollSpy } from './useScrollSpy'; + +import { Button } from '../../components'; + +const sections = ['1', '2', '3', '4', '5']; + +const meta: Meta = { + tags: ['autodocs'], + title: 'Hooks/useScrollSpy', + parameters: { + references: ['useScrollSpy'], + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Demo: Story = { + render: function Render() { + const containerRef = useRef(null); + + const isDocsPage = window.location.href.includes('docs'); + + const activeId = useScrollSpy(sections, { + ...(containerRef.current ? { root: containerRef.current } : {}), + }); + + return ( +
+
+ {sections.map((section) => ( + + ))} +
+
+ {sections.map((section, index) => ( +
+ {section} +
+ ))} +
+
+ ); + }, +}; diff --git a/packages/react/src/hooks/useScrollSpy/useScrollSpy.ts b/packages/react/src/hooks/useScrollSpy/useScrollSpy.ts new file mode 100644 index 000000000..e4cf038b8 --- /dev/null +++ b/packages/react/src/hooks/useScrollSpy/useScrollSpy.ts @@ -0,0 +1,38 @@ +import { useEffect, useState } from 'react'; + +import { useCallbackThrottle } from '../useCallbackThrottle'; +import { useEvent } from '../useEvent'; + +/** + * The hook that observes the intersection of elements with an ancestor or viewport and determines the active one. + * @param items An array of ids of elements to be observed. + * @param options An options object allowing you to set options for the observation. + * @param throttleDelay The time in milliseconds used to throttle the observation. + */ +export const useScrollSpy = (items: string[], options?: IntersectionObserverInit, throttleDelay = 0): string => { + const [activeElementId, setActiveElementId] = useState(''); + + const callback = useEvent( + useCallbackThrottle((entries: IntersectionObserverEntry[]) => { + entries.forEach((e) => e.isIntersecting && setActiveElementId(e.target.id)); + }, throttleDelay) + ); + + useEffect(() => { + const observer = new IntersectionObserver(callback, options); + + items.forEach((id) => { + const element = document.getElementById(id); + + if (element) { + observer.observe(element); + } + }); + + return () => { + observer.disconnect(); + }; + }, [items]); + + return activeElementId; +};