import * as React from 'react' ; import KeyCode from 'rc-util/lib/KeyCode' ; import raf from 'rc-util/lib/raf' ; import { getFocusNodeList } from 'rc-util/lib/Dom/focus' ; import type { MenuMode } from '../interface' ; import { getMenuId } from '../context/IdContext' ; const { LEFT , RIGHT , UP , DOWN , ENTER , ESC , HOME , END } = KeyCode ; const ArrowKeys = [ UP , DOWN , LEFT , RIGHT ] ; function getOffset ( mode , isRootLevel , isRtl , which ) { const prev = 'prev' as const ; const next = 'next' as const ; const children = 'children' as const ; const parent = 'parent' as const ; if ( mode === 'inline' && which === ENTER ) { return { inlineTrigger : true , } ; } type OffsetMap = Record < number , 'prev' | 'next' | 'children' | 'parent' > ; const inline = { [ UP ] : prev , [ DOWN ] : next , } ; const horizontal = { [ LEFT ] : isRtl ? next : prev , [ RIGHT ] : isRtl ? prev : next , [ DOWN ] : children , [ ENTER ] : children , } ; const vertical = { [ UP ] : prev , [ DOWN ] : next , [ ENTER ] : children , [ ESC ] : parent , [ LEFT ] : isRtl ? children : parent , [ RIGHT ] : isRtl ? parent : children , } ; const offsets = { inline , horizontal , vertical , inlineSub : inline , horizontalSub : vertical , verticalSub : vertical , } ; const type = offsets [ `${ mode }${ isRootLevel ? '' : 'Sub' }` ] ?. [ which ] ; switch ( type ) { case prev : return { offset : - 1 , sibling : true , } ; case next : return { offset : 1 , sibling : true , } ; case parent : return { offset : - 1 , sibling : false , } ; case children : return { offset : 1 , sibling : false , } ; default : return null ; } } function findContainerUL ( element ) { let current = element ; while ( current ) { if ( current . getAttribute ( 'data-menu-list' ) ) { return current as HTMLUListElement ; } current = current . parentElement ; } return null ; } /**
 * Find focused element within element set provided
 */ function getFocusElement ( activeElement , elements ) { let current = activeElement || document . activeElement ; while ( current ) { if ( elements . has ( current as any ) ) { return current as HTMLElement ; } current = current . parentElement ; } return null ; } /**
 * Get focusable elements from the element set under provided container
 */ function getFocusableElements ( container , elements ) { const list = getFocusNodeList ( container , true ) ; return list . filter ( ele => elements . has ( ele ) ) ; } function getNextFocusElement ( parentQueryContainer , elements , focusMenuElement ? , offset = 1 ) { if ( ! parentQueryContainer ) { return null ; } const sameLevelFocusableMenuElementList = getFocusableElements ( parentQueryContainer , elements ) ; const count = sameLevelFocusableMenuElementList . length ; let focusIndex = sameLevelFocusableMenuElementList . findIndex ( ele => focusMenuElement === ele ) ; if ( offset < 0 ) { if ( focusIndex === - 1 ) { focusIndex = count - 1 ; } else { focusIndex -= 1 ; } } else if ( offset > 0 ) { focusIndex += 1 ; } focusIndex = ( focusIndex + count ) % count ; return sameLevelFocusableMenuElementList [ focusIndex ] ; } export default function useAccessibility < T extends HTMLElement > ( mode , activeKey , isRtl , id , containerRef , getKeys , getKeyPath , triggerActiveKey , triggerAccessibilityOpen , originOnKeyDown ? ) { const rafRef = React . useRef < number > ( ) ; const activeRef = React . useRef < string > ( ) ; activeRef . current = activeKey ; const cleanRaf = ( ) => { raf . cancel ( rafRef . current ) ; } ; React . useEffect ( ( ) => ( ) => { cleanRaf ( ) ; } , [ ] ) ; return e => { const { which } = e ; if ( [ ... ArrowKeys , ENTER , ESC , HOME , END ] . includes ( which ) ) { let elements ; let key2element ; let element2key ; const refreshElements = ( ) => { elements = new Set < HTMLElement > ( ) ; key2element = new Map ( ) ; element2key = new Map ( ) ; const keys = getKeys ( ) ; keys . forEach ( key => { const element = document . querySelector ( `[data-menu-id='${ getMenuId ( id , key ) }']` ) as HTMLElement ; if ( element ) { elements . add ( element ) ; element2key . set ( element , key ) ; key2element . set ( key , element ) ; } } ) ; return elements ; } ; refreshElements ( ) ; const activeElement = key2element . get ( activeKey ) ; const focusMenuElement = getFocusElement ( activeElement , elements ) ; const focusMenuKey = element2key . get ( focusMenuElement ) ; const offsetObj = getOffset ( mode , getKeyPath ( focusMenuKey , true ) . length === 1 , isRtl , which ) ; if ( ! offsetObj && which !== HOME && which !== END ) { return ; } if ( ArrowKeys . includes ( which ) || [ HOME , END ] . includes ( which ) ) { e . preventDefault ( ) ; } const tryFocus = ( menuElement ) => { if ( menuElement ) { let focusTargetElement = menuElement ; const link = menuElement . querySelector ( 'a' ) ; if ( link ?. getAttribute ( 'href' ) ) { focusTargetElement = link ; } const targetKey = element2key . get ( menuElement ) ; triggerActiveKey ( targetKey ) ; /**
                     * Do not `useEffect` here since `tryFocus` may trigger async
                     * which makes React sync update the `activeKey`
                     * that force render before `useRef` set the next activeKey
                     */ cleanRaf ( ) ; rafRef . current = raf ( ( ) => { if ( activeRef . current === targetKey ) { focusTargetElement . focus ( ) ; } } ) ; } } ; if ( [ HOME , END ] . includes ( which ) || offsetObj . sibling || ! focusMenuElement ) { let parentQueryContainer ; if ( ! focusMenuElement || mode === 'inline' ) { parentQueryContainer = containerRef . current ; } else { parentQueryContainer = findContainerUL ( focusMenuElement ) ; } let targetElement ; const focusableElements = getFocusableElements ( parentQueryContainer , elements ) ; if ( which === HOME ) { targetElement = focusableElements [ 0 ] ; } else if ( which === END ) { targetElement = focusableElements [ focusableElements . length - 1 ] ; } else { targetElement = getNextFocusElement ( parentQueryContainer , elements , focusMenuElement , offsetObj . offset ) ; } tryFocus ( targetElement ) ; } else if ( offsetObj . inlineTrigger ) { triggerAccessibilityOpen ( focusMenuKey ) ; } else if ( offsetObj . offset > 0 ) { triggerAccessibilityOpen ( focusMenuKey , true ) ; cleanRaf ( ) ; rafRef . current = raf ( ( ) => { refreshElements ( ) ; const controlId = focusMenuElement . getAttribute ( 'aria-controls' ) ; const subQueryContainer = document . getElementById ( controlId ) ; const targetElement = getNextFocusElement ( subQueryContainer , elements ) ; tryFocus ( targetElement ) ; } , 5 ) ; } else if ( offsetObj . offset < 0 ) { const keyPath = getKeyPath ( focusMenuKey , true ) ; const parentKey = keyPath [ keyPath . length - 2 ] ; const parentMenuElement = key2element . get ( parentKey ) ; triggerAccessibilityOpen ( parentKey , false ) ; tryFocus ( parentMenuElement ) ; } } originOnKeyDown ?. ( e ) ; } ; }