import * as React from 'react'; import { useIsomorphicLayoutEffect } from '@fluentui/react-utilities'; import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts'; const { useCallback, useState, useRef } = React; import { useMutationObserver } from './useMutationObserver'; /** * This function will take the rootMargin and flip the sides if we are in RTL based on the computed reading direction of the target element. * @param ltrRootMargin the margin to be processed and flipped if required * @param target target element that will have its current reading direction determined * @returns the corrected rootMargin (if it was necessary to correct) */ export const getRTLRootMargin = (ltrRootMargin, target, win)=>{ if (target && win) { // get the computed dir for the target element const newDir = win.getComputedStyle(target).direction; // If we're in rtl reading direction, we might need to flip the margins on the left/right sides if (newDir === 'rtl') { let newMargin = ltrRootMargin; const splitMargins = ltrRootMargin.split(' '); // We only need to do this if we get four values, otherwise the sides are equal and don't require flipping. if (splitMargins.length === 4) { newMargin = `${splitMargins[0]} ${splitMargins[3]} ${splitMargins[2]} ${splitMargins[1]}`; } return newMargin; } else { return ltrRootMargin; } } return ltrRootMargin; }; /** * React hook that allows easy usage of the browser API IntersectionObserver within React * @param callback - A function called when the percentage of the target element is visible crosses a threshold. * @param options - An optional object which customizes the observer. If options isn't specified, the observer uses the * document's viewport as the root, with no margin, and a 0% threshold (meaning that even a one-pixel change is * enough to trigger a callback). * @returns An array containing a callback to update the list of Elements the observer should listen to, a callback to * update the init options of the IntersectionObserver and a ref to the IntersectionObserver instance itself. */ export const useIntersectionObserver = (callback, options)=>{ 'use no memo'; // TODO: exclude types from this lint rule: https://github.com/microsoft/fluentui/issues/31286 // eslint-disable-next-line no-restricted-globals const observer = useRef(); const [observerList, setObserverList] = useState(); const { targetDocument } = useFluent(); const win = targetDocument === null || targetDocument === void 0 ? void 0 : targetDocument.defaultView; var _options_rootMargin; // set the initial init with corrected margins based on the observed root's calculated reading direction. const [observerInit, setObserverInit] = useState(options && { ...options, rootMargin: getRTLRootMargin((_options_rootMargin = options.rootMargin) !== null && _options_rootMargin !== void 0 ? _options_rootMargin : '0px', options.root, win) }); var _options_rootMargin1; // We have to assume that any values passed in for rootMargin by the consuming app are ltr values. As such we will store the ltr value. const ltrRootMargin = useRef((_options_rootMargin1 = options === null || options === void 0 ? void 0 : options.rootMargin) !== null && _options_rootMargin1 !== void 0 ? _options_rootMargin1 : '0px'); // Callback function to execute when mutations are observed const mutationObserverCallback = useCallback((mutationList)=>{ for (const mutation of mutationList){ // Ensuring that the right attribute is being observed and that the root is within the tree of the element being mutated. if (mutation.type === 'attributes' && mutation.attributeName === 'dir' && (options === null || options === void 0 ? void 0 : options.root) && mutation.target.contains(options === null || options === void 0 ? void 0 : options.root)) { setObserverInit({ ...observerInit, rootMargin: getRTLRootMargin(ltrRootMargin.current, observerInit === null || observerInit === void 0 ? void 0 : observerInit.root, win) }); } } }, [ ltrRootMargin, observerInit, options === null || options === void 0 ? void 0 : options.root, win ]); // Mutation observer for dir attribute changes in the document useMutationObserver(targetDocument, mutationObserverCallback, { attributes: true, subtree: true, attributeFilter: [ 'dir' ] }); // Observer elements in passed in list and clean up previous list // This effect is only triggered when observerList is updated useIsomorphicLayoutEffect(()=>{ if (!win) { return; } observer.current = new win.IntersectionObserver(callback, { ...observerInit, rootMargin: getRTLRootMargin(ltrRootMargin.current, observerInit === null || observerInit === void 0 ? void 0 : observerInit.root, win) }); // If we have an instance of IO and a list with elements, observer the elements if (observer.current && observerList && observerList.length > 0) { observerList.forEach((element)=>{ var _observer_current; (_observer_current = observer.current) === null || _observer_current === void 0 ? void 0 : _observer_current.observe(element); }); } // clean up previous elements being listened to return ()=>{ if (observer.current) { observer.current.disconnect(); } }; }, [ observerList, observerInit, callback, win ]); // Do not use internally, we need to track external settings only here const setObserverInitExternal = useCallback((newInit)=>{ var _newInit_rootMargin; // Since we know this is coming from consumers, we can store this value as LTR somewhat safely. ltrRootMargin.current = (_newInit_rootMargin = newInit === null || newInit === void 0 ? void 0 : newInit.rootMargin) !== null && _newInit_rootMargin !== void 0 ? _newInit_rootMargin : '0px'; // Call the internal setter to update the value and ensure if our calculated direction is rtl, we flip the margin setObserverInit({ ...newInit, rootMargin: getRTLRootMargin(ltrRootMargin.current, newInit === null || newInit === void 0 ? void 0 : newInit.root, win) }); }, [ ltrRootMargin, setObserverInit, win ]); return { setObserverList, setObserverInit: setObserverInitExternal, observer }; };