import { useState, useEffect, useCallback, useRef } from 'react';
import { getAllPopovers, isInstance, normalizeTargets } from '../utils';
import useConfiguration from './useConfiguration';
const getElements = (els) => normalizeTargets(els)
    .flatMap(el => [el, ...getAllPopovers(el)])
    .filter((el) => isInstance(el, HTMLElement));
/** Hook for properly handling focus state of children components.
 * @example const hasFocus = useFocusWithin([containerRef, ...], (isFocused, element) => { doSomething; });
 * @param onFocusChange Callback function that is invoked with the current focus state and the current element when any child elements takes focus or all of them lose focus.
 * @returns * hasFocus:: A boolean indicating if the ref element has focus or not.
 */
const useFocusWithin = (els, onFocusChange) => {
    const [hasFocus, setFocus] = useState(false);
    const focusedElRef = useRef(null);
    const { portalTarget } = useConfiguration();
    const windowContext = portalTarget?.ownerDocument?.defaultView || window;
    const handlerAbortControllerRef = useRef();
    useEffect(() => {
        return () => {
            handlerAbortControllerRef.current?.abort();
        };
    }, []);
    const onBlur = useCallback((e) => {
        if (!hasFocus)
            return;
        const relatedTarget = isInstance(e, FocusEvent) ? e.relatedTarget : null;
        // Gather observed elements along with their popovers to probe for element which gains focus.
        const elements = normalizeTargets(els).flatMap(el => [el, ...getAllPopovers(el)]);
        if (isInstance(relatedTarget, Node)) {
            // changing focus to another relevant child doesn't count
            const focusedEl = elements.find(el => el?.contains(relatedTarget));
            if (focusedEl) {
                // ... just update the currently focused item
                focusedElRef.current = focusedEl;
                return;
            }
        }
        // Code below is similar to the useOuterEvent hook, but single-shot only and need to be attached
        // after blur event only. Due to various order of events between browsers and OSes, we need to
        // postpone calling 'onBlur' callback until next element gets focus or on outer click to check
        // if the relevant element contains the target element, which means the focus is retained.
        const handlerAbortController = new AbortController();
        const handler = (fe) => {
            if (!elements.some(el => {
                return fe.composedPath().some(target => {
                    return isInstance(target, Node) && el.contains(target);
                });
            })) {
                setFocus(false);
                onFocusChange?.(false, focusedElRef.current);
                focusedElRef.current = null;
            }
            handlerAbortController.abort();
        };
        // For keyboard and SR navigation - wait for 'focusin'.
        windowContext?.document?.addEventListener('focusin', handler, {
            once: true,
            signal: handlerAbortController.signal
        });
        // For mouse and touch navigation - wait for 'mouseup'.
        // The chosen event need to occur before 'click' to allow the given callback code finish before
        // 'click' handlers.
        windowContext?.document?.addEventListener('mouseup', handler, {
            once: true,
            signal: handlerAbortController.signal
        });
        // For parent window we do not need to verify element placement in DOM.
        const parentWindowHandler = () => {
            setFocus(false);
            onFocusChange?.(false, focusedElRef.current);
            focusedElRef.current = null;
            handlerAbortController.abort();
        };
        try {
            // When in iframe, we need to listen to parent window events as well.
            window.parent?.document.addEventListener('focusin', parentWindowHandler, {
                once: true,
                signal: handlerAbortController.signal
            });
            window.parent?.document.addEventListener('mouseup', parentWindowHandler, {
                once: true,
                signal: handlerAbortController.signal
            });
        }
        catch {
            // IFrame parent document is not accessible when cross-origin.
            // Don't attach handlers in that case.
        }
        handlerAbortControllerRef.current = handlerAbortController;
    }, [hasFocus, onFocusChange, els]);
    const onFocus = useCallback((e) => {
        if (!hasFocus) {
            setFocus(true);
            const targetEl = e.currentTarget;
            onFocusChange?.(true, targetEl);
            focusedElRef.current = targetEl;
        }
    }, [hasFocus, onFocusChange]);
    useEffect(() => {
        const elements = getElements(els);
        // Programmatically trigger focus callback on initial render to handle autoFocus
        elements.forEach(el => {
            if (el?.contains(document.activeElement)) {
                onFocus(new FocusEvent('focusin', { relatedTarget: document.activeElement }));
            }
        });
    }, []);
    useEffect(() => {
        const elements = normalizeTargets(els);
        elements.forEach(el => {
            el?.addEventListener('focusin', onFocus);
            el?.addEventListener('popover:focusin', onFocus);
            el?.addEventListener('focusout', onBlur);
            el?.addEventListener('popover:focusout', onBlur);
        });
        return () => {
            elements.forEach(el => {
                el?.removeEventListener('focusin', onFocus);
                el?.removeEventListener('popover:focusin', onFocus);
                el?.removeEventListener('focusout', onBlur);
                el?.removeEventListener('popover:focusout', onBlur);
            });
        };
    }, [els, onFocus, onBlur]);
    return hasFocus;
};
export default useFocusWithin;
//# sourceMappingURL=useFocusWithin.js.map