import React from 'react' ; import classnames from 'classnames' ; import { MDCDismissibleDrawerFoundation , MDCModalDrawerFoundation , util , } from '@material/drawer' ; import { MDCListFoundation } from '@material/list/foundation' ; import DrawerHeader from './Header' ; import DrawerContent from './Content' ; import DrawerSubtitle from './Subtitle' ; import DrawerTitle from './Title' ; import DrawerAppContent from './AppContent' ; import { FocusTrap } from 'focus-trap' ; const { cssClasses : listCssClasses } = MDCListFoundation ; type RefCallback < T > = ( node : T ) => void ; export interface DrawerProps extends React . HTMLProps < HTMLElement > { className ? : string ; open ? : boolean ; onOpen ? : ( ) => void ; onClose ? : ( ) => void ; tag ? : string ; dismissible ? : boolean ; modal ? : boolean ; innerRef ? : RefCallback < HTMLElement > | React . RefObject < HTMLElement > ; } interface DrawerState { classList : Set < string > ; } const isRefObject = function ( ref ) : ref is React . RefObject < HTMLElement > { return typeof ref !== 'function' ; } ; class Drawer extends React . Component < DrawerProps , DrawerState > { previousFocus : HTMLElement | null = null ; foundation ? : MDCDismissibleDrawerFoundation | MDCModalDrawerFoundation ; focusTrap ? : FocusTrap ; drawerElement : React . RefObject < HTMLDivElement > = React . createRef ( ) ; state : DrawerState = { classList : new Set ( ) } ; static defaultProps : Partial < DrawerProps > = { className : '' , children : null , open : false , onOpen : ( ) => { } , onClose : ( ) => { } , onTransitionEnd : ( ) => { } , onKeyDown : ( ) => { } , tag : 'aside' , dismissible : false , modal : false , } ; componentDidMount ( ) { const { open } = this . props ; this . initFoundation ( ) ; if ( open && this . foundation ) { this . foundation . open ( ) ; } } private initFoundation = ( ) => { const { dismissible , modal } = this . props ; if ( this . foundation ) { this . foundation . destroy ( ) ; } if ( dismissible ) { this . foundation = new MDCDismissibleDrawerFoundation ( this . adapter ) ; this . foundation . init ( ) ; } else if ( modal ) { this . initializeFocusTrap ( ) ; this . foundation = new MDCModalDrawerFoundation ( this . adapter ) ; this . foundation . init ( ) ; } } ; componentDidUpdate ( prevProps ) { const { dismissible , modal , open } = this . props ; const changedToModal = prevProps . modal !== this . props . modal ; const changedToDismissible = prevProps . dismissible !== this . props . dismissible ; if ( ! dismissible && ! modal ) return ; if ( changedToModal || changedToDismissible ) { this . initFoundation ( ) ; } if ( open !== prevProps . open && this . foundation ) { open ? this . foundation . open ( ) : this . foundation . close ( ) ; } } componentWillUnmount ( ) { if ( ! this . foundation ) return ; this . foundation . destroy ( ) ; } private initializeFocusTrap = ( ) => { this . focusTrap = util . createFocusTrapInstance ( this . drawerElement . current ! ) ; } ; get classes ( ) { const { classList } = this . state ; const { className , dismissible , modal } = this . props ; return classnames ( 'mdc-drawer' , Array . from ( classList ) , className , { 'mdc-drawer--dismissible' : dismissible , 'mdc-drawer--modal' : modal , } ) ; } get adapter ( ) { return { addClass : ( className ) => { const { classList } = this . state ; classList . add ( className ) ; this . setState ( { classList } ) ; } , removeClass : ( className ) => { const { classList } = this . state ; classList . delete ( className ) ; this . setState ( { classList } ) ; } , hasClass : ( className ) => this . classes . split ( ' ' ) . includes ( className ) , elementHasClass : ( element , className ) => element . classList . contains ( className ) , saveFocus : ( ) => { this . previousFocus = document . activeElement as HTMLElement | null ; } , restoreFocus : ( ) => { const drawerElement = this . drawerElement && this . drawerElement . current ; if ( drawerElement && this . previousFocus && this . previousFocus . focus && drawerElement . contains ( document . activeElement ) ) { this . previousFocus . focus ( ) ; } } , focusActiveNavigationItem : ( ) => { const drawerElement = this . drawerElement && this . drawerElement . current ; if ( ! drawerElement ) return ; const activeNavItemEl = drawerElement . querySelector ( `.${ listCssClasses . LIST_ITEM_ACTIVATED_CLASS }` ) as HTMLElement | null ; if ( activeNavItemEl ) { activeNavItemEl . focus ( ) ; } } , notifyClose : this . props . onClose , notifyOpen : this . props . onOpen , trapFocus : ( ) => { if ( this . focusTrap ) { this . focusTrap . activate ( ) ; } } , releaseFocus : ( ) => { if ( this . focusTrap ) { this . focusTrap . deactivate ( ) ; } } , } ; } handleKeyDown = ( evt ) => { this . props . onKeyDown ! ( evt ) ; if ( ! this . foundation ) return ; this . foundation . handleKeydown ( evt . nativeEvent ) ; } ; handleTransitionEnd = ( evt ) => { this . props . onTransitionEnd ! ( evt ) ; if ( ! this . foundation ) return ; this . foundation . handleTransitionEnd ( evt . nativeEvent ) ; } ; attachRef = ( node ) => { const { innerRef } = this . props ; this . drawerElement . current = node ; if ( ! innerRef ) { return ; } if ( isRefObject ( innerRef ) ) { innerRef . current = node ; } else { innerRef ( node ) ; } } ; render ( ) { const { onClose , onOpen , onKeyDown , onTransitionEnd , dismissible , children , className , innerRef , modal , tag : Tag , ... otherProps } = this . props ; return ( < React . Fragment > { } < Tag className = { this . classes } ref = { this . attachRef } onKeyDown = { this . handleKeyDown } onTransitionEnd = { this . handleTransitionEnd } { ... otherProps } > { children } < /Tag> { modal ? this . renderScrim ( ) : null } < /React.Fragment>); } renderScrim ( ) { return ( < div className = 'mdc-drawer-scrim' onClick = { ( ) => ( this . foundation as MDCModalDrawerFoundation ) . handleScrimClick ( ) } />); } } export default Drawer ; export { DrawerHeader , DrawerSubtitle , DrawerTitle , DrawerContent , DrawerAppContent , } ;