import React , { forwardRef , HTMLAttributes , ReactNode , useContext , useMemo , useRef , useState } from 'react' ; import ClickAwayListener from '@material-ui/core/ClickAwayListener' ; import Grow from '@material-ui/core/Grow' ; import { PopperPlacementType } from '@material-ui/core/Popper' ; import { PopperOptions } from 'popper.js' ; import RootRef from '@material-ui/core/RootRef' ; import { makeStyles , Theme } from '@material-ui/core/styles' ; import cx from 'classnames' ; import { spacingToRem , SpacingType , StandardProps } from '@toptal/picasso-shared' ; import DropdownArrow from '../DropdownArrow' ; import Popper from '../Popper' ; import Paper from '../Paper' ; import styles from './styles' ; import noop from '../utils/noop' ; type ContentOverflowType = 'scroll' | 'visible' ; export interface Props extends StandardProps , HTMLAttributes < HTMLDivElement > { /** Anchor element that opens content on click */ children : ReactNode ; /** Content element that opens when anchor is clicked */ content : ReactNode ; /** Offset of content element relative to anchor element */ offset ? : { top ? : SpacingType ; bottom ? : SpacingType ; left ? : SpacingType ; right ? : SpacingType ; } ; placement ? : PopperPlacementType ; /** Disable auto focus of first item in list or item */ disableAutoFocus ? : boolean ; /** Disable close on generic close events */ disableAutoClose ? : boolean ; /** Disable the portal behavior. The children stay within it's parent DOM hierarchy. */ disablePortal ? : boolean ; popperOptions ? : PopperOptions ; /** Callback invoked when component is opened */ onOpen ? : ( ) => void ; /** Callback invoked when component is closed */ onClose ? : ( ) => void ; /** Sets the desired behavior for an element's overflow */ contentOverflow ? : ContentOverflowType ; popperContainer ? : HTMLElement ; } interface ContextProps { close : ( ) => void ; } const DropdownContext = React . createContext < ContextProps | null > ( null ) ; const useDropdownContext = ( ) => { const context = useContext ( DropdownContext ) ; if ( ! context ) { throw new Error ( 'Dropdown compound components cannot be rendered outside the Dropdown component' ) ; } return context ; } ; const useStyles = makeStyles < Theme , Props > ( styles , { name : 'PicassoDropdown' } ) ; export const Dropdown = forwardRef < HTMLDivElement , Props > ( function Dropdown ( props , ref ) { const { className , style , children , content , offset , placement , disableAutoClose , disableAutoFocus , disablePortal , popperOptions , onOpen = noop , popperContainer , onClose = noop , classes : externalClasses , contentOverflow = 'scroll' , ... rest } = props ; const classes = useStyles ( props ) ; const contentRef = useRef < HTMLElement > ( ) ; const [ anchorEl , setAnchorEl ] = useState < HTMLDivElement | undefined > ( ) ; const [ isOpen , setIsOpen ] = useState ( false ) ; const handleAnchorClick = ( event ) => { if ( isOpen ) { close ( ) ; } else { open ( event ) ; } } ; const handleContentKeyDown = ( event ) => { if ( event . key === 'Tab' ) { event . preventDefault ( ) ; } if ( event . key === 'Escape' ) { forceClose ( ) ; } if ( event . key === 'Enter' ) { close ( ) ; } if ( event . key === ' ' ) { close ( ) ; } } ; const open = ( event ) => { setAnchorEl ( event . currentTarget ) ; setIsOpen ( true ) ; onOpen ( ) ; } ; const close = ( ) => { if ( disableAutoClose ) { return ; } forceClose ( ) ; } ; const forceClose = ( ) => { setAnchorEl ( undefined ) ; setIsOpen ( false ) ; onClose ( ) ; } ; const focus = ( ) => { if ( disableAutoFocus ) { return ; } if ( ! contentRef || ! contentRef . current ) { return ; } const { firstChild } = contentRef . current ; if ( firstChild && firstChild . focus ) { return firstChild . focus ( ) ; } if ( contentRef . current . focus ) { return contentRef . current . focus ( ) ; } } ; const paperMargins = useMemo ( ( ) => { if ( offset ) { return { ... ( offset . top && { marginTop : spacingToRem ( offset . top ) } ) , ... ( offset . bottom && { marginBottom : spacingToRem ( offset . bottom ) } ) , ... ( offset . left && { marginLeft : spacingToRem ( offset . left ) } ) , ... ( offset . right && { marginRight : spacingToRem ( offset . right ) } ) } ; } } , [ offset ] ) ; const context = { close : ( ) => forceClose ( ) } ; const handleClickAway = ( event ) => { const target = event . target ; const isAnchorTapEvent = anchorEl && target instanceof Node && anchorEl . contains ( target ) ; if ( isAnchorTapEvent ) { return ; } forceClose ( ) ; } ; return ( < div { ... rest } ref = { ref } className = { cx ( classes . root , className ) } style = { style } > < div className = { classes . anchor } onClick = { handleAnchorClick } > { typeof children === 'function' ? children ( { isOpen } ) : children } < /div> { anchorEl && isOpen && ( < Popper className = { classes . popper } anchorEl = { anchorEl } popperOptions = { { onCreate : focus , modifiers : { flip : { enabled : contentOverflow !== 'visible' } } , ... popperOptions } } placement = { placement } style = { paperMargins } disablePortal = { disablePortal } autoWidth = { false } open enableCompactMode container = { popperContainer } > < ClickAwayListener onClickAway = { handleClickAway } > { } { } < div onClick = { close } > < Grow in = { isOpen } appear > < Paper className = { cx ( classes . content , { [ classes . contentVisible ] : contentOverflow === 'visible' } ) } onKeyDown = { handleContentKeyDown } elevation = { 2 } > < DropdownContext . Provider value = { context } > < RootRef rootRef = { contentRef } > { content } < /RootRef> < /DropdownContext.Provider> < /Paper> < /Grow> < /div> < /ClickAwayListener> < /Popper>)} < /div>); } ) ; Dropdown . defaultProps = { disableAutoClose : false , disableAutoFocus : true , disablePortal : false , onClose : noop , onOpen : noop , placement : 'bottom-end' , popperOptions : { } } ; Dropdown . displayName = 'Dropdown' ; export default Object . assign ( Dropdown , { Arrow : DropdownArrow , useContext : useDropdownContext } ) ;