diff --git a/docs/src/mdx/data/mdx-hooks-data.ts b/docs/src/mdx/data/mdx-hooks-data.ts index 4a44640518a..c500adbd4d4 100644 --- a/docs/src/mdx/data/mdx-hooks-data.ts +++ b/docs/src/mdx/data/mdx-hooks-data.ts @@ -128,4 +128,5 @@ export const MDX_HOOKS_DATA: Record = { useWindowScroll: hDocs('useWindowScroll', 'Tracks window scroll position'), usePagination: hDocs('usePagination', 'Manages pagination state'), + useIsVisible: hDocs('useIsVisible', 'Detects if element is visible in viewport'), }; diff --git a/docs/src/mdx/mdx-pages-group.ts b/docs/src/mdx/mdx-pages-group.ts index 2c195d2452e..87a78662b0b 100644 --- a/docs/src/mdx/mdx-pages-group.ts +++ b/docs/src/mdx/mdx-pages-group.ts @@ -83,6 +83,7 @@ export const MDX_PAGES_GROUPS: MdxPagesGroup[] = [ MDX_DATA.useViewportSize, MDX_DATA.useWindowEvent, MDX_DATA.useWindowScroll, + MDX_DATA.useIsVisible, ], }, diff --git a/docs/src/pages/hooks/use-is-visible.mdx b/docs/src/pages/hooks/use-is-visible.mdx new file mode 100644 index 00000000000..8740f36c2a6 --- /dev/null +++ b/docs/src/pages/hooks/use-is-visible.mdx @@ -0,0 +1,11 @@ +import { HooksDemos } from '@docs/demos'; +import { Layout } from '@/layout'; +import { MDX_DATA } from '@/mdx'; + +export default Layout(MDX_DATA.useIsVisible); + +## Usage + +`use-is-visible` is a hook that returns a boolean value indicating whether the target element is visible in the viewport. It makes use of the [Intersection Observer API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API) to determine visibility. + + diff --git a/packages/@docs/demos/src/demos/hooks/Hooks.demos.story.tsx b/packages/@docs/demos/src/demos/hooks/Hooks.demos.story.tsx index d1f2e228279..f7dea212564 100644 --- a/packages/@docs/demos/src/demos/hooks/Hooks.demos.story.tsx +++ b/packages/@docs/demos/src/demos/hooks/Hooks.demos.story.tsx @@ -262,3 +262,8 @@ export const DemoUseEyeDropperUsage = { name: '⭐ Demo: useEyeDropperUsage', render: renderDemo(demos.useEyeDropperUsage), }; + +export const DemoUseIsVisibleUsage = { + name: '⭐ Demo: useIsVisibleUsage', + render: renderDemo(demos.useIsVisibleDemo), +}; diff --git a/packages/@docs/demos/src/demos/hooks/index.ts b/packages/@docs/demos/src/demos/hooks/index.ts index 50ee1c084a6..ec6285bb927 100644 --- a/packages/@docs/demos/src/demos/hooks/index.ts +++ b/packages/@docs/demos/src/demos/hooks/index.ts @@ -50,3 +50,4 @@ export { useTextSelectionUsage } from './use-text-selection.demo.usage'; export { usePreviousUsage } from './use-previous.demo.usage'; export { useFaviconUsage } from './use-favicon.demo.usage'; export { useEyeDropperUsage } from './use-eye-dropper.demo.usage'; +export { useIsVisibleDemo } from './use-is-visible.demo.usage'; diff --git a/packages/@docs/demos/src/demos/hooks/use-is-visible.demo.usage.tsx b/packages/@docs/demos/src/demos/hooks/use-is-visible.demo.usage.tsx new file mode 100644 index 00000000000..73369232e6c --- /dev/null +++ b/packages/@docs/demos/src/demos/hooks/use-is-visible.demo.usage.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { Box, Text } from '@mantine/core'; +import { useIsVisible } from '@mantine/hooks'; +import { MantineDemo } from '@mantinex/demo'; + +const code = ` +import { useIsVisible } from '@mantine/hooks'; + +function Demo() { + const { ref, isVisible } = useIsVisible(); + return ( + <> + {isVisible ? 'Box is visible' : 'Scroll to see box'} + + + + + A box + + + + + ); +} +`; + +function Demo() { + const { ref, isVisible } = useIsVisible(); + return ( + <> + {isVisible ? 'Box is visible' : 'Scroll to see box'} + + + + + A box + + + + + ); +} + +export const useIsVisibleDemo: MantineDemo = { + type: 'code', + code, + component: Demo, +}; diff --git a/packages/@mantine/hooks/src/index.ts b/packages/@mantine/hooks/src/index.ts index 344d23b7e1f..a0514d0cf90 100644 --- a/packages/@mantine/hooks/src/index.ts +++ b/packages/@mantine/hooks/src/index.ts @@ -57,6 +57,7 @@ export { usePrevious } from './use-previous/use-previous'; export { useFavicon } from './use-favicon/use-favicon'; export { useHeadroom } from './use-headroom/use-headroom'; export { useEyeDropper } from './use-eye-dropper/use-eye-dropper'; +export { useIsVisible } from './use-is-visible/use-is-visible'; export type { UseMovePosition } from './use-move/use-move'; export type { OS } from './use-os/use-os'; diff --git a/packages/@mantine/hooks/src/use-is-visible/use-is-visible.ts b/packages/@mantine/hooks/src/use-is-visible/use-is-visible.ts new file mode 100644 index 00000000000..df1677d34df --- /dev/null +++ b/packages/@mantine/hooks/src/use-is-visible/use-is-visible.ts @@ -0,0 +1,23 @@ +import { useEffect, useMemo, useRef, useState } from 'react'; + +export function useIsVisible() { + const ref = useRef(null); + const [isVisible, setIsVisible] = useState(false); + + const observer = useMemo(() => { + if (typeof IntersectionObserver === 'undefined') { + return null; + } + return new IntersectionObserver(([entry]) => setIsVisible(entry.isIntersecting)); + }, [ref]); + + useEffect(() => { + if (ref.current && observer) { + observer.observe(ref.current); + return () => observer.disconnect(); + } + return () => null; + }, []); + + return { ref, isVisible }; +}