toggle-corp/react-store

View on GitHub
v2/View/FloatingContainer/index.tsx

Summary

Maintainability
A
3 hrs
Test Coverage
import React, { useRef, useEffect, useCallback } from 'react';
import { _cs } from '@togglecorp/fujs';

import Float from '../Float';
// import Haze from '../Haze';

import useHaze from '../../General/useHaze';
import { Keys } from '../../types';

import styles from './styles.scss';

/*
# Breaking Change
- Change prop parent to parentRef

# Todo
- Merge with Float
- onBlur and onClose should be merged into same function
*/

interface Props {
    className?: string;
    closeOnEscape: boolean;
    focusTrap: boolean;
    onBlur?: () => void;
    onClose?: (attributes: { escape: boolean }) => void;
    onInvalidate?: (e: HTMLDivElement) => Record<string, unknown>; // gets container
    onMouseDown?: (e: MouseEvent) => void; // gets mouse down event
    parentRef: React.RefObject<HTMLElement>;
    showHaze: boolean;
    children?: React.ReactNode;
}

/* Float with haze, close on outside click, and close on escape */
function FloatingContainer(props: Props) {
    const {
        children,
        className: classNameFromProps,
        closeOnEscape,
        focusTrap,
        onBlur,
        onClose,
        onInvalidate,
        onMouseDown,
        parentRef,
        showHaze,
    } = props;

    const containerRef = useRef<HTMLDivElement>(null);

    const handleContainerInvalidate = useCallback(
        () => {
            const { current: container } = containerRef;
            if (container && onInvalidate) {
                const containerStyles = onInvalidate(container);
                if (containerStyles) {
                    Object.assign(container.style, containerStyles);
                } else {
                    console.error('FloatingContainer.onInvalidate should return style');
                }
            }
        },
        [
            containerRef,
            onInvalidate,
        ],
    );

    const handleMouseDown = useCallback(
        (e: MouseEvent) => {
            const { current: container } = containerRef;
            const { current: parent } = parentRef;

            const isTargetOrContainsTarget = container && (
                container === e.target || container.contains(e.target as HTMLElement)
            );

            const isTargetParentOrContainedInParent = parent && (
                parent === e.target || parent.contains(e.target as HTMLElement)
            );

            const clickedInside = isTargetOrContainsTarget || isTargetParentOrContainedInParent;

            if (!clickedInside) {
                if (onBlur) {
                    onBlur();
                }
            } else if (onMouseDown) {
                onMouseDown(e);
            }
        },
        [
            onBlur,
            onMouseDown,
            parentRef,
            containerRef,
        ],
    );

    const handleKeyPressed = useCallback(
        (event: KeyboardEvent) => {
            const { current: container } = containerRef;
            const isLastModal = container && container.dataset.lastModal === 'true';
            if (isLastModal && onClose && closeOnEscape && event.keyCode === Keys.Esc) {
                onClose({ escape: true });
            }
        },
        [
            closeOnEscape,
            onClose,
            containerRef,
        ],
    );

    useEffect(
        () => {
            window.addEventListener('mousedown', handleMouseDown);
            return () => {
                window.removeEventListener('mousedown', handleMouseDown);
            };
        },
        [handleMouseDown],
    );

    useEffect(
        () => {
            document.addEventListener('keydown', handleKeyPressed);
            return () => {
                document.removeEventListener('keydown', handleKeyPressed);
            };
        },
        [handleKeyPressed],
    );

    const myClassName = _cs(
        classNameFromProps,
        styles.floatingContainer,
        'floating-container',
    );

    const [containerId, className] = useHaze(myClassName, showHaze);

    return (
        <Float
            onInvalidate={handleContainerInvalidate}
            focusTrap={focusTrap}
        >
            <div
                id={containerId}
                className={className}
                ref={containerRef}
            >
                { children }
            </div>
        </Float>
    );
}
FloatingContainer.defaultProps = {
    closeOnEscape: false,
    focusTrap: false,
    showHaze: false,
};
export default FloatingContainer;