import * as React from "react" ; import { Browser } from "roosterjs-editor-dom" ; import { closest , css , NullFunction } from "roosterjs-react-common" ; export type FocusEventHandler = ( ev : React . FocusEvent < HTMLElement > ) => void ; export interface FocusOutShellProps { allowMouseDown ? : ( target : HTMLElement ) => boolean ; className ? : string ; onBlur ? : FocusEventHandler ; onFocus ? : FocusEventHandler ; children : ( calloutClassName : string , calloutOnDismiss : FocusEventHandler ) => React . ReactNode ; shouldCallBlur ? : ( nextTarget : HTMLElement , shouldCallBlurDefault : ( nextTarget : HTMLElement ) => boolean ) => boolean ; } export interface FocusOutShellState { isFocused ? : boolean ; } export default class FocusOutShell extends React . PureComponent < FocusOutShellProps , FocusOutShellState > { private static readonly BaseClassName = "focus-out-shell" ; private static readonly CalloutClassName = `${ FocusOutShell . BaseClassName }-callout` ; private static NextId = 0 ; private _calloutClassName = `${ FocusOutShell . CalloutClassName }-${ FocusOutShell . NextId ++ }` ; private _containerDiv : HTMLDivElement ; constructor ( props ) { super ( props ) ; this . state = { isFocused : false } ; } public render ( ) { const { className , children } = this . props ; return ( < div className = { css ( FocusOutShell . BaseClassName , className , { "is-focused" : this . state . isFocused } ) } ref = { this . _containerDivOnRef } onBlur = { this . _onBlur } onFocus = { this . _onFocus } onMouseDown = { this . _onMouseDown } > { children ( this . _calloutClassName , this . _calloutOnDismiss ) } < /div>); } private _calloutOnDismiss = ( ev ) : void => { if ( ! ev ) { return ; } const nextTarget = ev . target as HTMLElement ; if ( this . _shouldCallBlur ( nextTarget ) ) { requestAnimationFrame ( ( ) => { const { onBlur = NullFunction } = this . props ; onBlur ( ev ) ; this . setState ( { isFocused : false } ) ; } ) ; } } ; private _onBlur = ( ev ) : void => { const nextTarget = ev . relatedTarget as HTMLElement ; if ( this . _shouldCallBlur ( nextTarget ) ) { const { onBlur = NullFunction } = this . props ; onBlur ( ev ) ; this . setState ( { isFocused : false } ) ; } } ; private _shouldCallBlur ( nextTarget ) { const { shouldCallBlur = this . _shouldCallBlurDefault } = this . props ; return shouldCallBlur ( nextTarget , this . _shouldCallBlurDefault ) ; } private _shouldCallBlurDefault = ( nextTarget ) : boolean => { if ( nextTarget && this . _containerDiv . contains ( nextTarget ) ) { return false ; } if ( nextTarget == null && Browser . isIE ) { nextTarget = document . activeElement as HTMLElement ; } if ( nextTarget && closest ( nextTarget , `.${ this . _calloutClassName }` ) ) { return false ; } return true ; } ; private _onFocus = ( ev ) : void => { if ( ! this . state . isFocused ) { const { onFocus = NullFunction } = this . props ; onFocus ( ev ) ; } } ; private _onFocusNative = ( ev ) : void => { this . setState ( { isFocused : true } ) ; } ; private _onMouseDown = ( ev ) : void => { const { allowMouseDown = NullFunction } = this . props ; const { target } = ev ; if ( allowMouseDown ( target as HTMLElement ) ) { return ; } ev . preventDefault ( ) ; } ; private _containerDivOnRef = ( ref ) : void => { this . _containerDiv = ref ; const eventName = "focusin" ; if ( this . _containerDiv ) { this . _containerDiv . removeEventListener ( eventName , this . _onFocusNative ) ; } if ( ref ) { ref . addEventListener ( eventName , this . _onFocusNative ) ; } } ; }