license import { MDCComponent } from '@material/base/component' ; import { SpecificEventListener } from '@material/base/types' ; import { MDCMenuSurfaceAdapter } from './adapter' ; import { Corner , cssClasses , strings } from './constants' ; import { MDCMenuSurfaceFoundation } from './foundation' ; import { MDCMenuDistance } from './types' ; import { getCorrectPropertyName } from '@material/animation/util' ; type RegisterFunction = ( ) => void ; export type MDCMenuSurfaceFactory = ( el : Element , foundation ? : MDCMenuSurfaceFoundation ) => MDCMenuSurface ; export class MDCMenuSurface extends MDCComponent < MDCMenuSurfaceFoundation > { static override attachTo ( root ) { return new MDCMenuSurface ( root ) ; } anchorElement ! : Element | null ; private previousFocus ? : HTMLElement | SVGElement | null ; private handleKeydown ! : SpecificEventListener < 'keydown' > ; private handleBodyClick ! : SpecificEventListener < 'click' > ; private registerBodyClickListener ! : RegisterFunction ; private deregisterBodyClickListener ! : RegisterFunction ; override initialSyncWithDOM ( ) { const parentEl = this . root . parentElement ; this . anchorElement = parentEl && parentEl . classList . contains ( cssClasses . ANCHOR ) ? parentEl : null ; if ( this . root . classList . contains ( cssClasses . FIXED ) ) { this . setFixedPosition ( true ) ; } this . handleKeydown = ( event ) => { this . foundation . handleKeydown ( event ) ; } ; this . handleBodyClick = ( event ) => { this . foundation . handleBodyClick ( event ) ; } ; this . registerBodyClickListener = ( ) => { document . body . addEventListener ( 'click' , this . handleBodyClick , { capture : true } ) ; } ; this . deregisterBodyClickListener = ( ) => { document . body . removeEventListener ( 'click' , this . handleBodyClick , { capture : true } ) ; } ; this . listen ( 'keydown' , this . handleKeydown ) ; this . listen ( strings . OPENED_EVENT , this . registerBodyClickListener ) ; this . listen ( strings . CLOSED_EVENT , this . deregisterBodyClickListener ) ; } override destroy ( ) { this . unlisten ( 'keydown' , this . handleKeydown ) ; this . unlisten ( strings . OPENED_EVENT , this . registerBodyClickListener ) ; this . unlisten ( strings . CLOSED_EVENT , this . deregisterBodyClickListener ) ; super . destroy ( ) ; } isOpen ( ) { return this . foundation . isOpen ( ) ; } open ( ) { this . foundation . open ( ) ; } close ( skipRestoreFocus = false ) { this . foundation . close ( skipRestoreFocus ) ; } set quickOpen ( quickOpen ) { this . foundation . setQuickOpen ( quickOpen ) ; } /** Sets the foundation to use page offsets for an positioning when the menu is hoisted to the body. */ setIsHoisted ( isHoisted ) { this . foundation . setIsHoisted ( isHoisted ) ; } /** Sets the element that the menu-surface is anchored to. */ setMenuSurfaceAnchorElement ( element ) { this . anchorElement = element ; } /** Sets the menu-surface to position: fixed. */ setFixedPosition ( isFixed ) { if ( isFixed ) { this . root . classList . add ( cssClasses . FIXED ) ; } else { this . root . classList . remove ( cssClasses . FIXED ) ; } this . foundation . setFixedPosition ( isFixed ) ; } /** Sets the absolute x/y position to position based on. Requires the menu to be hoisted. */ setAbsolutePosition ( x , y ) { this . foundation . setAbsolutePosition ( x , y ) ; this . setIsHoisted ( true ) ; } param corner setAnchorCorner ( corner ) { this . foundation . setAnchorCorner ( corner ) ; } setAnchorMargin ( margin ) { this . foundation . setAnchorMargin ( margin ) ; } override getDefaultFoundation ( ) { const adapter = { addClass : ( className ) => this . root . classList . add ( className ) , removeClass : ( className ) => this . root . classList . remove ( className ) , hasClass : ( className ) => this . root . classList . contains ( className ) , hasAnchor : ( ) => ! ! this . anchorElement , notifyClose : ( ) => this . emit ( MDCMenuSurfaceFoundation . strings . CLOSED_EVENT , { } ) , notifyClosing : ( ) => { this . emit ( MDCMenuSurfaceFoundation . strings . CLOSING_EVENT , { } ) ; } , notifyOpen : ( ) => this . emit ( MDCMenuSurfaceFoundation . strings . OPENED_EVENT , { } ) , isElementInContainer : ( el ) => this . root . contains ( el ) , isRtl : ( ) => getComputedStyle ( this . root ) . getPropertyValue ( 'direction' ) === 'rtl' , setTransformOrigin : ( origin ) => { const propertyName = `${ getCorrectPropertyName ( window , 'transform' ) }-origin` ; ( this . root as HTMLElement ) . style . setProperty ( propertyName , origin ) ; } , isFocused : ( ) => document . activeElement === this . root , saveFocus : ( ) => { this . previousFocus = document . activeElement as HTMLElement | SVGElement | null ; } , restoreFocus : ( ) => { if ( this . root . contains ( document . activeElement ) ) { if ( this . previousFocus && this . previousFocus . focus ) { this . previousFocus . focus ( ) ; } } } , getInnerDimensions : ( ) => { return { width : ( this . root as HTMLElement ) . offsetWidth , height : ( this . root as HTMLElement ) . offsetHeight } ; } , getAnchorDimensions : ( ) => this . anchorElement ? this . anchorElement . getBoundingClientRect ( ) : null , getWindowDimensions : ( ) => { return { width : window . innerWidth , height : window . innerHeight } ; } , getBodyDimensions : ( ) => { return { width : document . body . clientWidth , height : document . body . clientHeight } ; } , getWindowScroll : ( ) => { return { x : window . pageXOffset , y : window . pageYOffset } ; } , setPosition : ( position ) => { const rootHTML = this . root as HTMLElement ; rootHTML . style . left = 'left' in position ? `${ position . left }px` : '' ; rootHTML . style . right = 'right' in position ? `${ position . right }px` : '' ; rootHTML . style . top = 'top' in position ? `${ position . top }px` : '' ; rootHTML . style . bottom = 'bottom' in position ? `${ position . bottom }px` : '' ; } , setMaxHeight : ( height ) => { ( this . root as HTMLElement ) . style . maxHeight = height ; } , } ; return new MDCMenuSurfaceFoundation ( adapter ) ; } }