main

mattermost/focalboard

Last updated at: 29/12/2023 09:44

menuWrapper.tsx

TLDR

This file contains the implementation of the MenuWrapper component, a React component that wraps a menu and manages its visibility and behavior.

Methods

close

This method is used to close the menu. It sets the open state to false and triggers the onToggle callback with false as the argument.

closeOnBlur

This method is called when a click event occurs outside the menu. It checks if the target of the click event is within the menu and if not, it calls the close method.

keyboardClose

This method is called when a keyboard event occurs. If the key is "Escape" or "Tab", it calls the close or closeOnBlur method accordingly.

toggle

This method is called when a click event occurs on the menu. It toggles the open state and triggers the onToggle callback with the updated value.

Classes

There are no classes in this file.

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.

import React, {useRef, useState, useEffect, useCallback} from 'react'

import './menuWrapper.scss'

type Props = {
    children?: React.ReactNode
    stopPropagationOnToggle?: boolean
    className?: string
    disabled?: boolean
    isOpen?: boolean
    onToggle?: (open: boolean) => void
    label?: string
}

const MenuWrapper = (props: Props) => {
    const node = useRef<HTMLDivElement>(null)
    const [open, setOpen] = useState(Boolean(props.isOpen))

    if (!Array.isArray(props.children) || props.children.length !== 2) {
        throw new Error('MenuWrapper needs exactly 2 children')
    }

    const close = useCallback((): void => {
        if (open) {
            setOpen(false)
            props.onToggle && props.onToggle(false)
        }
    }, [props.onToggle, open])

    const closeOnBlur = useCallback((e: Event) => {
        if (e.target && node.current?.contains(e.target as Node)) {
            return
        }

        close()
    }, [close])

    const keyboardClose = useCallback((e: KeyboardEvent) => {
        if (e.key === 'Escape') {
            close()
        }

        if (e.key === 'Tab') {
            closeOnBlur(e)
        }
    }, [close, closeOnBlur])

    const toggle = useCallback((e: React.MouseEvent<HTMLDivElement, MouseEvent>): void => {
        if (props.disabled) {
            return
        }

        /**
         * This is only here so that we can toggle the menus in the sidebar, because the default behavior of the mobile
         * version (ie the one that uses a modal) needs propagation to close the modal after selecting something
         * We need to refactor this so that the modal is explicitly closed on toggle, but for now I am aiming to preserve the existing logic
         * so as to not break other things
        **/
        if (props.stopPropagationOnToggle) {
            e.preventDefault()
            e.stopPropagation()
        }
        setOpen(!open)
        props.onToggle && props.onToggle(!open)
    }, [props.onToggle, open, props.disabled])

    useEffect(() => {
        if (open) {
            document.addEventListener('menuItemClicked', close, true)
            document.addEventListener('click', closeOnBlur, true)
            document.addEventListener('keyup', keyboardClose, true)
        }
        return () => {
            if (open) {
                document.removeEventListener('menuItemClicked', close, true)
                document.removeEventListener('click', closeOnBlur, true)
                document.removeEventListener('keyup', keyboardClose, true)
            }
        }
    }, [open, close, closeOnBlur, keyboardClose])

    const {children} = props
    let className = 'MenuWrapper'
    if (props.disabled) {
        className += ' disabled'
    }
    if (open) {
        className += ' override menuOpened'
    }
    if (props.className) {
        className += ' ' + props.className
    }

    return (
        <div
            role='button'
            aria-label={props.label || 'menuwrapper'}
            className={className}
            onClick={toggle}
            ref={node}
        >
            {children ? Object.values(children)[0] : null}
            {children && !props.disabled && open ? Object.values(children)[1] : null}
        </div>
    )
}

export default React.memo(MenuWrapper)