@ -6,6 +6,8 @@ import Overlay from 'react-overlays/lib/Overlay';
import Motion from '../features/ui/util/optional_motion' ;
import Motion from '../features/ui/util/optional_motion' ;
import spring from 'react-motion/lib/spring' ;
import spring from 'react-motion/lib/spring' ;
import { supportsPassiveEvents } from 'detect-passive-events' ;
import { supportsPassiveEvents } from 'detect-passive-events' ;
import classNames from 'classnames' ;
import { CircularProgress } from 'mastodon/components/loading_indicator' ;
const listenerOptions = supportsPassiveEvents ? { passive : true } : false ;
const listenerOptions = supportsPassiveEvents ? { passive : true } : false ;
let id = 0 ;
let id = 0 ;
@ -17,13 +19,18 @@ class DropdownMenu extends React.PureComponent {
} ;
} ;
static propTypes = {
static propTypes = {
items : PropTypes . array . isRequired ,
items : PropTypes . oneOfType ( [ PropTypes . array , ImmutablePropTypes . list ] ) . isRequired ,
loading : PropTypes . bool ,
scrollable : PropTypes . bool ,
onClose : PropTypes . func . isRequired ,
onClose : PropTypes . func . isRequired ,
style : PropTypes . object ,
style : PropTypes . object ,
placement : PropTypes . string ,
placement : PropTypes . string ,
arrowOffsetLeft : PropTypes . string ,
arrowOffsetLeft : PropTypes . string ,
arrowOffsetTop : PropTypes . string ,
arrowOffsetTop : PropTypes . string ,
openedViaKeyboard : PropTypes . bool ,
openedViaKeyboard : PropTypes . bool ,
renderItem : PropTypes . func ,
renderHeader : PropTypes . func ,
onItemClick : PropTypes . func . isRequired ,
} ;
} ;
static defaultProps = {
static defaultProps = {
@ -45,9 +52,11 @@ class DropdownMenu extends React.PureComponent {
document . addEventListener ( 'click' , this . handleDocumentClick , false ) ;
document . addEventListener ( 'click' , this . handleDocumentClick , false ) ;
document . addEventListener ( 'keydown' , this . handleKeyDown , false ) ;
document . addEventListener ( 'keydown' , this . handleKeyDown , false ) ;
document . addEventListener ( 'touchend' , this . handleDocumentClick , listenerOptions ) ;
document . addEventListener ( 'touchend' , this . handleDocumentClick , listenerOptions ) ;
if ( this . focusedItem && this . props . openedViaKeyboard ) {
if ( this . focusedItem && this . props . openedViaKeyboard ) {
this . focusedItem . focus ( { preventScroll : true } ) ;
this . focusedItem . focus ( { preventScroll : true } ) ;
}
}
this . setState ( { mounted : true } ) ;
this . setState ( { mounted : true } ) ;
}
}
@ -66,7 +75,7 @@ class DropdownMenu extends React.PureComponent {
}
}
handleKeyDown = e => {
handleKeyDown = e => {
const items = Array . from ( this . node . getElementsByTagName( 'a ') ) ;
const items = Array . from ( this . node . querySelectorAll( 'a, button ') ) ;
const index = items . indexOf ( document . activeElement ) ;
const index = items . indexOf ( document . activeElement ) ;
let element = null ;
let element = null ;
@ -109,21 +118,11 @@ class DropdownMenu extends React.PureComponent {
}
}
handleClick = e => {
handleClick = e => {
const i = Number ( e . currentTarget . getAttribute ( 'data-index' ) ) ;
const { onItemClick } = this . props ;
const { action , to } = this . props . items [ i ] ;
onItemClick ( e ) ;
this . props . onClose ( ) ;
if ( typeof action === 'function' ) {
e . preventDefault ( ) ;
action ( e ) ;
} else if ( to ) {
e . preventDefault ( ) ;
this . context . router . history . push ( to ) ;
}
}
}
renderItem ( option , i ) {
renderItem = ( option , i ) => {
if ( option === null ) {
if ( option === null ) {
return < li key = { ` sep- ${ i } ` } className = 'dropdown-menu__separator' / > ;
return < li key = { ` sep- ${ i } ` } className = 'dropdown-menu__separator' / > ;
}
}
@ -140,9 +139,11 @@ class DropdownMenu extends React.PureComponent {
}
}
render ( ) {
render ( ) {
const { items , style , placement , arrowOffsetLeft , arrowOffsetTop } = this . props ;
const { items , style , placement , arrowOffsetLeft , arrowOffsetTop , scrollable , renderHeader , loading } = this . props ;
const { mounted } = this . state ;
const { mounted } = this . state ;
let renderItem = this . props . renderItem || this . renderItem ;
return (
return (
< Motion defaultStyle = { { opacity : 0 , scaleX : 0.85 , scaleY : 0.75 } } style = { { opacity : spring ( 1 , { damping : 35 , stiffness : 400 } ) , scaleX : spring ( 1 , { damping : 35 , stiffness : 400 } ) , scaleY : spring ( 1 , { damping : 35 , stiffness : 400 } ) } } >
< Motion defaultStyle = { { opacity : 0 , scaleX : 0.85 , scaleY : 0.75 } } style = { { opacity : spring ( 1 , { damping : 35 , stiffness : 400 } ) , scaleX : spring ( 1 , { damping : 35 , stiffness : 400 } ) , scaleY : spring ( 1 , { damping : 35 , stiffness : 400 } ) } } >
{ ( { opacity , scaleX , scaleY } ) => (
{ ( { opacity , scaleX , scaleY } ) => (
@ -152,9 +153,23 @@ class DropdownMenu extends React.PureComponent {
< div className = { ` dropdown-menu ${ placement } ` } style = { { ... style , opacity : opacity , transform : mounted ? ` scale( ${ scaleX } , ${ scaleY } ) ` : null } } ref = { this . setRef } >
< div className = { ` dropdown-menu ${ placement } ` } style = { { ... style , opacity : opacity , transform : mounted ? ` scale( ${ scaleX } , ${ scaleY } ) ` : null } } ref = { this . setRef } >
< div className = { ` dropdown-menu__arrow ${ placement } ` } style = { { left : arrowOffsetLeft , top : arrowOffsetTop } } / >
< div className = { ` dropdown-menu__arrow ${ placement } ` } style = { { left : arrowOffsetLeft , top : arrowOffsetTop } } / >
< ul >
< div className = { classNames ( 'dropdown-menu__container' , { 'dropdown-menu__container--loading' : loading } ) } >
{ items . map ( ( option , i ) => this . renderItem ( option , i ) ) }
{ loading && (
< / u l >
< CircularProgress size = { 30 } strokeWidth = { 3.5 } / >
) }
{ ! loading && renderHeader && (
< div className = 'dropdown-menu__container__header' >
{ renderHeader ( items ) }
< / d i v >
) }
{ ! loading && (
< ul className = { classNames ( 'dropdown-menu__container__list' , { 'dropdown-menu__container__list--scrollable' : scrollable } ) } >
{ items . map ( ( option , i ) => renderItem ( option , i , { onClick : this . handleClick , onKeyPress : this . handleItemKeyPress } ) ) }
< / u l >
) }
< / d i v >
< / d i v >
< / d i v >
) }
) }
< / M o t i o n >
< / M o t i o n >
@ -170,11 +185,14 @@ export default class Dropdown extends React.PureComponent {
} ;
} ;
static propTypes = {
static propTypes = {
icon : PropTypes . string . isRequired ,
children : PropTypes . node ,
items : PropTypes . array . isRequired ,
icon : PropTypes . string ,
size : PropTypes . number . isRequired ,
items : PropTypes . oneOfType ( [ PropTypes . array , ImmutablePropTypes . list ] ) . isRequired ,
loading : PropTypes . bool ,
size : PropTypes . number ,
title : PropTypes . string ,
title : PropTypes . string ,
disabled : PropTypes . bool ,
disabled : PropTypes . bool ,
scrollable : PropTypes . bool ,
status : ImmutablePropTypes . map ,
status : ImmutablePropTypes . map ,
isUserTouching : PropTypes . func ,
isUserTouching : PropTypes . func ,
onOpen : PropTypes . func . isRequired ,
onOpen : PropTypes . func . isRequired ,
@ -182,6 +200,9 @@ export default class Dropdown extends React.PureComponent {
dropdownPlacement : PropTypes . string ,
dropdownPlacement : PropTypes . string ,
openDropdownId : PropTypes . number ,
openDropdownId : PropTypes . number ,
openedViaKeyboard : PropTypes . bool ,
openedViaKeyboard : PropTypes . bool ,
renderItem : PropTypes . func ,
renderHeader : PropTypes . func ,
onItemClick : PropTypes . func ,
} ;
} ;
static defaultProps = {
static defaultProps = {
@ -237,17 +258,21 @@ export default class Dropdown extends React.PureComponent {
}
}
handleItemClick = e => {
handleItemClick = e => {
const { onItemClick } = this . props ;
const i = Number ( e . currentTarget . getAttribute ( 'data-index' ) ) ;
const i = Number ( e . currentTarget . getAttribute ( 'data-index' ) ) ;
const { action , to } = this . props . items [ i ] ;
const item = this . props . items [ i ] ;
this . handleClose ( ) ;
this . handleClose ( ) ;
if ( typeof action === 'function' ) {
if ( typeof onItemClick === 'function' ) {
e . preventDefault ( ) ;
onItemClick ( item , i ) ;
} else if ( item && typeof item . action === 'function' ) {
e . preventDefault ( ) ;
e . preventDefault ( ) ;
action ( ) ;
item. action( ) ;
} else if ( to ) {
} else if ( item && item . to) {
e . preventDefault ( ) ;
e . preventDefault ( ) ;
this . context . router . history . push ( to) ;
this . context . router . history . push ( item. to) ;
}
}
}
}
@ -265,29 +290,67 @@ export default class Dropdown extends React.PureComponent {
}
}
}
}
close = ( ) => {
this . handleClose ( ) ;
}
render ( ) {
render ( ) {
const { icon , items , size , title , disabled , dropdownPlacement , openDropdownId , openedViaKeyboard } = this . props ;
const {
icon ,
items ,
size ,
title ,
disabled ,
loading ,
scrollable ,
dropdownPlacement ,
openDropdownId ,
openedViaKeyboard ,
children ,
renderItem ,
renderHeader ,
} = this . props ;
const open = this . state . id === openDropdownId ;
const open = this . state . id === openDropdownId ;
const button = children ? React . cloneElement ( React . Children . only ( children ) , {
ref : this . setTargetRef ,
onClick : this . handleClick ,
onMouseDown : this . handleMouseDown ,
onKeyDown : this . handleButtonKeyDown ,
onKeyPress : this . handleKeyPress ,
} ) : (
< IconButton
icon = { icon }
title = { title }
active = { open }
disabled = { disabled }
size = { size }
ref = { this . setTargetRef }
onClick = { this . handleClick }
onMouseDown = { this . handleMouseDown }
onKeyDown = { this . handleButtonKeyDown }
onKeyPress = { this . handleKeyPress }
/ >
) ;
return (
return (
< div >
< React . Fragment >
< IconButton
{ button }
icon = { icon }
title = { title }
active = { open }
disabled = { disabled }
size = { size }
ref = { this . setTargetRef }
onClick = { this . handleClick }
onMouseDown = { this . handleMouseDown }
onKeyDown = { this . handleButtonKeyDown }
onKeyPress = { this . handleKeyPress }
/ >
< Overlay show = { open } placement = { dropdownPlacement } target = { this . findTarget } >
< Overlay show = { open } placement = { dropdownPlacement } target = { this . findTarget } >
< DropdownMenu items = { items } onClose = { this . handleClose } openedViaKeyboard = { openedViaKeyboard } / >
< DropdownMenu
items = { items }
loading = { loading }
scrollable = { scrollable }
onClose = { this . handleClose }
openedViaKeyboard = { openedViaKeyboard }
renderItem = { renderItem }
renderHeader = { renderHeader }
onItemClick = { this . handleItemClick }
/ >
< / O v e r l a y >
< / O v e r l a y >
< / d i v >
< / Re a c t . F r a g m e n t >
) ;
) ;
}
}