import { computed , getCurrentInstance , onBeforeMount , onBeforeUnmount , ref , Ref , ToRefs , toRefs , unref , VNodeProps , watch , watchEffect , } from "vue" ; import { useIds } from "@chakra-ui/vue-composables" ; import { FocusLockProps , useFocusLock } from "@chakra-ui/c-focus-lock" ; import { MaybeElementRef , useRef , getSelector , TemplateRef , } from "@chakra-ui/vue-utils" ; import { hideOthers , Undo } from "@chakra-ui/vue-a11y" ; import { focus , FocusableElement , isFunction } from "@chakra-ui/utils" ; import { useBodyScrollLock } from "@chakra-ui/c-scroll-lock" ; export interface UseModalOptions { /**
     * If `true`, the modal will be open.
     */ modelValue : Ref < boolean > ; /**
     * The `id` of the modal
     */ id ? : Ref < string > ; default closeOnOverlayClick ? : Ref < boolean > ; default blockScrollOnMount ? : Ref < boolean > ; /**
     * The initial element to be focused when the focus lock is opened
     */ initialFocusRef ? : Ref < FocusLockProps [ "initialFocusRef" ] > ; /**
     * The initial element to be focused when the focus lock is opened
     */ finalFocusRef ? : Ref < FocusLockProps [ "finalFocusRef" ] > ; default closeOnEsc ? : Ref < boolean > ; default useInert ? : Ref < boolean > ; /**
     * Emits event to close modal dialog
     */ closeModal : ( ) => void ; /**
     * Emits `escape` event to parent scope
     */ handleEscape : ( event : KeyboardEvent ) => void ; } export interface MergedVNodeProps extends VNodeProps { ref : TemplateRef | ( ( el : TemplateRef | null ) => void ) ; } param options returns export function useModal ( options ) { const { handleEscape , closeModal } = options ; const { modelValue , id , closeOnOverlayClick , closeOnEsc , initialFocusRef , finalFocusRef , useInert , blockScrollOnMount , } = toRefs ( options ) ; const instance = getCurrentInstance ( ) ; const finalFocusElement = computed ( ( ) => { let finalFocus ; if ( finalFocusRef ?. value ) { const resolvedFinalFocusRef = isFunction ( finalFocusRef . value ) ? finalFocusRef . value ?. ( ) : finalFocusRef . value ; if ( typeof resolvedFinalFocusRef === "string" ) { finalFocus = document . querySelector < FocusableElement & Element > ( resolvedFinalFocusRef ) ; } else { finalFocus = resolvedFinalFocusRef ?. $el || resolvedFinalFocusRef ; } } return finalFocus ; } ) ; const initialFocusElement = computed ( ( ) => { let initialFocus ; if ( initialFocusRef ?. value ) { let resolvedInitialFocusRef = typeof initialFocusRef ?. value === "function" ? initialFocusRef ?. value ( ) : initialFocusRef ?. value ; resolvedInitialFocusRef = unref ( resolvedInitialFocusRef ) ; if ( typeof resolvedInitialFocusRef === "string" ) { initialFocus = document . querySelector < FocusableElement & Element > ( resolvedInitialFocusRef ) ; } else { initialFocus = resolvedInitialFocusRef ?. $el || resolvedInitialFocusRef ; } } return initialFocus ; } ) ; const [ dialogRef , dialogRefEl ] = useRef ( ) ; const [ overlayRef , overlayEl ] = useRef ( ) ; /** We use this element to keep track of the currently clicked element */ const mouseDownTarget = ref < EventTarget | null > ( null ) ; /**
     * Creates IDs for the dialog elements
     */ const [ dialogId , headerId , bodyId ] = useIds ( id ?. value , `chakra-modal` , `chakra-modal--header` , `chakra-modal--body` ) ; const { lastFocusedSelector } = useReturnFocusSelector ( modelValue ) ; const hasHeader = ref ( false ) ; const hasBody = ref ( false ) ; /** Initialize focus lock */ const { lock , deactivate } = useFocusLock ( { escapeDeactivates : false , clickOutsideDeactivates : false , allowOutsideClick : true , returnFocusOnDeactivate : true , delayInitialFocus : true , onActivate ( ) { if ( initialFocusElement . value ) { setTimeout ( ( ) => { focus ( initialFocusElement . value ) ; } ) ; } } , onDeactivate ( ) { see useReturnFocusSelector setTimeout ( ( ) => { const lastfocusedNode = document . querySelector ( lastFocusedSelector . value as string ) ; if ( finalFocusElement . value ) { focus ( finalFocusElement . value ) ; } else { focus ( lastfocusedNode as HTMLElement ) ; } } , 100 ) ; } , immediate : true , } ) ; const { scrollLockRef } = useBodyScrollLock ( blockScrollOnMount ?. value ? modelValue : ref ( false ) ) ; /**
     * This watcher is being used to track
     * the element refs for the dialog container
     * element.
     *
     * When the ref is bound, we activate
     * the focus lock and body scroll lock refs.
     */ watch ( dialogRefEl , ( newVal ) => { if ( newVal ) { lock ( newVal ) ; scrollLockRef ( newVal ) ; } else { deactivate ( ) ; } } ) ; /**
     * Dialog props
     */ const dialogProps = computed < ( context : any ) => MergedVNodeProps > ( ( ) => ( { emit } ) => ( { role : "dialog" , ref : dialogRef , id : dialogId . value , tabIndex : - 1 , "aria-modal" : true , "aria-labelledby" : hasHeader . value ? headerId . value : null , "aria-describedby" : hasBody . value ? bodyId . value : null , onClick ( event ) { event . stopPropagation ( ) ; emit ( "click" , event ) ; } , } ) ) ; const handleOverlayClick = ( event ) => { event . stopPropagation ( ) ; if ( event . target !== event . currentTarget ) return ; if ( closeOnOverlayClick ?. value ) { closeModal ( ) ; } } ; const onKeyDown = ( event ) => { if ( event . key === "Escape" ) { event . stopPropagation ( ) ; if ( closeOnEsc ?. value ) { closeModal ( ) ; } handleEscape ( event ) ; } } ; /** Dialog container props */ const dialogContainerProps = computed < ( context : any ) => MergedVNodeProps > ( ( ) => ( { emit } ) => ( { ref : overlayRef as any , onClick : ( event ) => { instance ?. emit ( "update:modelValue" , ! modelValue . value ) ; instance ?. emit ( "closeModal" ) ; handleOverlayClick ( event ) ; } , onKeydown : ( event ) => { emit ( "keydown" , event ) ; onKeyDown ( event ) ; } , onMousedown : ( event ) => { mouseDownTarget . value = event . target ; emit ( "mousedown" , event ) ; } , } ) ) ; see useAriaHidden const shouldHide = computed ( ( ) => ( modelValue . value && useInert ?. value ) || false ) ; useAriaHidden ( dialogRefEl , shouldHide ) ; return { modelValue , headerId , bodyId , dialogRef , dialogRefEl , overlayRef , dialogProps , hasHeader , hasBody , dialogContainerProps , } ; } export type UseModalReturn = Omit < ToRefs < ReturnType < typeof useModal > > , "dialogRef" | "overlayRef" | "closeModal" > ; param ref param shouldHide export function useAriaHidden ( node , shouldHide ) { let undo = null ; watchEffect ( ( ) => { if ( shouldHide . value && node . value ) { undo = hideOthers ( node . value ) ; } else { undo ?. ( ) ; } } , { flush : "post" , } ) ; } /** Tracks last focused element selector before Modal/dialog is opened */ export function useReturnFocusSelector ( shouldTrack ) { const lastFocused = ref < EventTarget | null > ( null ) ; const lastFocusedSelector = ref < string | undefined > ( ) ; const trackFocus = ( event ) => { if ( ! shouldTrack . value ) { lastFocusedSelector . value = getSelector ( event . target as HTMLElement ) ; } } ; onBeforeMount ( ( ) => { document . addEventListener ( "focusin" , trackFocus ) ; } ) ; onBeforeUnmount ( ( ) => { document . removeEventListener ( "focusin" , trackFocus ) ; lastFocused . value = null ; lastFocusedSelector . value = undefined ; } ) ; return { lastFocused , lastFocusedSelector , } ; }