import { FocusScope , FocusScopeIterator } from "./useFocusScope" ; import { isFunction , isNil } from "./assertions" ; import { ChildScopesOptions } from "." ; import { FocusTarget } from "./focusTarget" ; import { useMemo } from "react" ; export interface FocusManagerOptions { keyProp ? : string ; onFocus ? : ( targetElement : HTMLElement ) => void ; } export interface FocusManagerHandlers { onFocus ? : ( targetElement : HTMLElement ) => void ; onNotFound ? : ( ) => void ; } export interface FocusManagerScopeOptions extends ChildScopesOptions { tabbableOnly ? : boolean ; } export interface FocusManagerIterationOptions { canFocus ? : ( element : HTMLElement ) => boolean ; } export interface FocusManager { focusFirst : ( options ? : FocusManagerScopeOptions & FocusManagerIterationOptions & FocusManagerHandlers ) => HTMLElement ; focusFirstQueryMatch : ( query : string , handlers ? : FocusManagerScopeOptions & FocusManagerHandlers ) => HTMLElement ; focusKey : ( key : string , options ? : FocusManagerScopeOptions & FocusManagerHandlers ) => HTMLElement ; focusLast : ( options ? : FocusManagerScopeOptions & FocusManagerIterationOptions & FocusManagerHandlers ) => HTMLElement ; focusNext : ( options ? : FocusManagerScopeOptions & FocusManagerIterationOptions & FocusManagerHandlers ) => HTMLElement ; focusPrevious : ( options ? : FocusManagerScopeOptions & FocusManagerIterationOptions & FocusManagerHandlers ) => HTMLElement ; focusTarget : ( key : string , options ? : FocusManagerScopeOptions & FocusManagerIterationOptions & FocusManagerHandlers ) => HTMLElement ; getActiveElementIndex : ( options ? : FocusManagerScopeOptions ) => number ; isInScope : ( element : HTMLElement , options ? : FocusManagerScopeOptions ) => boolean ; } abstract class FocusManagerBase { protected scope : FocusScope ; protected keyProp : string ; protected onFocus : ( element : HTMLElement ) => void ; constructor ( scope , { keyProp , onFocus } = { } ) { this . scope = scope ; this . keyProp = keyProp ; this . onFocus = onFocus ; } isInScope ( element , options ? ) { return this . scope . isInScope ( element , options ) ; } abstract getActiveElementIndex ( options ? ) ; abstract focusElement ( element , handlers ) ; focusFirst ( { canFocus , onFocus , onNotFound , ... scopeOptions } = { } ) { const iterator = new FocusScopeIterator ( this . scope , scopeOptions ) ; const element = iterator . firstElement ( { acceptElement : canFocus } ) ; this . focusElement ( element , { onFocus , onNotFound } ) ; return element ; } focusLast ( { canFocus , onFocus , onNotFound , ... scopeOptions } = { } ) { const iterator = new FocusScopeIterator ( this . scope , scopeOptions ) ; const element = iterator . lastElement ( { acceptElement : canFocus } ) ; this . focusElement ( element , { onFocus , onNotFound } ) ; return element ; } focusNext ( { canFocus , onFocus , onNotFound , ... scopeOptions } = { } ) { const from = this . getActiveElementIndex ( scopeOptions ) ; const iterator = new FocusScopeIterator ( this . scope , { from : from !== - 1 ? from : undefined , ... scopeOptions } ) ; const element = iterator . nextElement ( { acceptElement : canFocus } ) ; this . focusElement ( element , { onFocus , onNotFound } ) ; return element ; } focusPrevious ( { canFocus , onFocus , onNotFound , ... scopeOptions } = { } ) { const from = this . getActiveElementIndex ( scopeOptions ) ; const iterator = new FocusScopeIterator ( this . scope , { from : from !== - 1 ? from : undefined , ... scopeOptions } ) ; const element = iterator . previousElement ( { acceptElement : canFocus } ) ; this . focusElement ( element , { onFocus , onNotFound } ) ; return element ; } focusKey ( key , { onFocus , onNotFound , ... scopeOptions } = { } ) { const elements = this . scope . getElements ( scopeOptions ) ; if ( isNil ( this . keyProp ) ) { throw new Error ( "\"focusKey\" cannot be called without providing a `keyProp` to the FocusManager." ) ; } const element = elements . find ( ( x ) => x . getAttribute ( this . keyProp ) === key ?. toString ( ) ) ; this . focusElement ( element , { onFocus , onNotFound } ) ; return element ; } focusTarget ( target , options ? ) { switch ( target ) { case FocusTarget . first : return this . focusFirst ( options ) ; case FocusTarget . last : return this . focusLast ( options ) ; } return this . focusKey ( target , options ) ; } focusFirstQueryMatch ( query , { onFocus , onNotFound , ... scopeOptions } = { } ) { const element = this . scope . getElements ( scopeOptions ) . find ( ( x ) => x . textContent ?. toLowerCase ( ) . startsWith ( query ) ) ; this . focusElement ( element , { onFocus , onNotFound } ) ; return element ; } } export class DomFocusManager extends FocusManagerBase implements FocusManager { getActiveElementIndex ( options ? ) { return this . scope . getElements ( options ) . indexOf ( document . activeElement as HTMLElement ) ; } focusElement ( element , { onFocus , onNotFound } = { } ) { if ( ! isNil ( element ) ) { if ( isFunction ( element . focus ) ) { element . focus ( ) ; [ onFocus , this . onFocus ] . forEach ( x => { if ( ! isNil ( x ) ) { x ( element ) ; } } ) ; } } else { if ( ! isNil ( onNotFound ) ) { onNotFound ( ) ; } } } } export const VirtualFocusCssClass = "o-ui-focus" ; export class VirtualFocusManager extends FocusManagerBase implements FocusManager { getActiveElementIndex ( options ? ) { return this . scope . getElements ( options ) . findIndex ( ( x ) => x . classList . contains ( VirtualFocusCssClass ) ) ; } focusElement ( element , { onFocus , onNotFound , ... scopeOptions } = { } ) { if ( ! isNil ( element ) ) { this . scope . getElements ( scopeOptions ) . forEach ( ( x ) => { if ( x . classList . contains ( VirtualFocusCssClass ) ) { x . classList . remove ( VirtualFocusCssClass ) ; } } ) ; element . classList . add ( VirtualFocusCssClass ) ; [ onFocus , this . onFocus ] . forEach ( handler => { if ( ! isNil ( handler ) ) { handler ( element ) ; } } ) ; } else { if ( ! isNil ( onNotFound ) ) { onNotFound ( ) ; } } } getActiveElement ( options ? ) { return this . scope . getElements ( options ) . find ( ( x ) => x . classList . contains ( VirtualFocusCssClass ) ) ; } } export interface UseFocusManagerOptions extends FocusManagerOptions { isVirtual ? : boolean ; } export function useFocusManager ( scope , { isVirtual = false , keyProp , onFocus } = { } ) { return useMemo ( ( ) => { const options = { keyProp , onFocus } ; return isVirtual ? new VirtualFocusManager ( scope , options ) : new DomFocusManager ( scope , options ) ; } , [ scope , isVirtual , keyProp , onFocus ] ) ; }