diff --git a/src/index.ts b/src/index.ts index 4ddf5d5d..7af693a7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,6 +13,7 @@ export * from './useMutationObserver' export * from './useNow' export * from './usePatchRef' export * from './useResizeObserver' +export * from './usePositionStickyObserver' export * from './useRouteParam' export * from './useRouteQuery' export * from './useRouteQueryParam' diff --git a/src/usePositionStickyObserver/README.md b/src/usePositionStickyObserver/README.md new file mode 100644 index 00000000..1726b3db --- /dev/null +++ b/src/usePositionStickyObserver/README.md @@ -0,0 +1,34 @@ +# usePositionStickyObserver + +This composition is abstracts away the logic necessary to determine if a `position: sticky;` element has gone into it's "stuck" mode. This is useful when you want to style the element differently when it's stuck, like if you want to add a background color. + +## Example + +```typescript +const stickyHeader = ref() +const { stuck } = usePositionStickyObserver(stickyHeader) + +const classes = computed(() ({ + header: { + 'header--stuck': stuck.value, + } +})) +``` + +## Arguments + +| Name | Type | +| ------- | ---------------------------------------------- | +| element | `HTMLElement \| Ref` | +| options | '{}' | + +### Options + +| Name | Type | +| --------------- | ------------------------------------------------------------------------------------------------------ | +| rootMargin | `string`, [MDN DOcs](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/rootMargin) | +| boundingElement | `HTMLElement \| Ref`. The scroll container, defaults to the body. | + +## Returns + +`UsePositionStickyObserverResponse` diff --git a/src/usePositionStickyObserver/index.ts b/src/usePositionStickyObserver/index.ts new file mode 100644 index 00000000..3e7fe724 --- /dev/null +++ b/src/usePositionStickyObserver/index.ts @@ -0,0 +1 @@ +export * from './usePositionStickyObserver' \ No newline at end of file diff --git a/src/usePositionStickyObserver/usePositionStickyObserver.ts b/src/usePositionStickyObserver/usePositionStickyObserver.ts new file mode 100644 index 00000000..c1df2c51 --- /dev/null +++ b/src/usePositionStickyObserver/usePositionStickyObserver.ts @@ -0,0 +1,52 @@ +import { Ref, computed, onMounted, ref } from 'vue' +import { MaybeRefOrGetter } from '@/types/maybe' +import { useIntersectionObserver } from '@/useIntersectionObserver' +import { toValue } from '@/utilities/vue' + +export type UsePositionStickyObserverResponse = { + stuck: Ref, +} + +export type UsePositionStickyObserverOptions = { + rootMargin?: string, + boundingElement?: HTMLElement, +} + +const usePositionStickyObserverDefaultOptions = { + rootMargin: '-1px 0px 0px 0px', + boundingElement: document.body, +} + +export function usePositionStickyObserver( + element: MaybeRefOrGetter, + options?: MaybeRefOrGetter, +): UsePositionStickyObserverResponse { + const elementRef = computed(() => toValue(element)) + const stuck = ref(false) + + const observerOptions = computed(() => { + const { rootMargin: rootMarginOption, boundingElement: boundingElementOption } = toValue(options ?? {}) + const rootMargin = rootMarginOption ?? usePositionStickyObserverDefaultOptions.rootMargin + const root = boundingElementOption ?? usePositionStickyObserverDefaultOptions.boundingElement + + return { + threshold: [1], + rootMargin, + root, + } + }) + + function intersect(entries: IntersectionObserverEntry[]): void { + entries.forEach(entry => { + stuck.value = entry.intersectionRatio < 1 + }) + } + + const { observe } = useIntersectionObserver(intersect, observerOptions) + + onMounted(() => observe(elementRef)) + + return { + stuck, + } +} \ No newline at end of file