* Remove pointer events on the entire UI when a dropdown menu is open This prevents operations to change the location of the menu such as scrolling. * Fix mistake from mergemaster
@@ -0,0 +1,10 @@ | |||
export const DROPDOWN_MENU_OPEN = 'DROPDOWN_MENU_OPEN'; | |||
export const DROPDOWN_MENU_CLOSE = 'DROPDOWN_MENU_CLOSE'; | |||
export function openDropdownMenu(id, placement) { | |||
return { type: DROPDOWN_MENU_OPEN, id, placement }; | |||
} | |||
export function closeDropdownMenu(id) { | |||
return { type: DROPDOWN_MENU_CLOSE, id }; | |||
} |
@@ -8,6 +8,7 @@ import spring from 'react-motion/lib/spring'; | |||
import detectPassiveEvents from 'detect-passive-events'; | |||
const listenerOptions = detectPassiveEvents.hasSupport ? { passive: true } : false; | |||
let id = 0; | |||
class DropdownMenu extends React.PureComponent { | |||
@@ -124,8 +125,10 @@ export default class Dropdown extends React.PureComponent { | |||
status: ImmutablePropTypes.map, | |||
isUserTouching: PropTypes.func, | |||
isModalOpen: PropTypes.bool.isRequired, | |||
onModalOpen: PropTypes.func, | |||
onModalClose: PropTypes.func, | |||
onOpen: PropTypes.func.isRequired, | |||
onClose: PropTypes.func.isRequired, | |||
dropdownPlacement: PropTypes.string, | |||
openDropdownId: PropTypes.number, | |||
}; | |||
static defaultProps = { | |||
@@ -133,38 +136,28 @@ export default class Dropdown extends React.PureComponent { | |||
}; | |||
state = { | |||
placement: null, | |||
id: id++, | |||
}; | |||
handleClick = ({ target }) => { | |||
if (this.state.placement) { | |||
this.setState({ placement: null }); | |||
} else if (this.props.isUserTouching() && this.props.onModalOpen) { | |||
const { status, items } = this.props; | |||
this.props.onModalOpen({ | |||
status, | |||
actions: items, | |||
onClick: this.handleItemClick, | |||
}); | |||
if (this.state.id === this.props.openDropdownId) { | |||
this.handleClose(); | |||
} else { | |||
const { top } = target.getBoundingClientRect(); | |||
this.setState({ placement: top * 2 < innerHeight ? 'bottom' : 'top' }); | |||
const placement = top * 2 < innerHeight ? 'bottom' : 'top'; | |||
this.props.onOpen(this.state.id, this.handleItemClick, placement); | |||
} | |||
} | |||
handleClose = () => { | |||
if (this.props.onModalClose) { | |||
this.props.onModalClose(); | |||
} | |||
this.setState({ placement: null }); | |||
this.props.onClose(this.state.id); | |||
} | |||
handleKeyDown = e => { | |||
switch(e.key) { | |||
case 'Enter': | |||
this.handleClick(); | |||
this.handleClick(e); | |||
break; | |||
case 'Escape': | |||
this.handleClose(); | |||
@@ -196,23 +189,22 @@ export default class Dropdown extends React.PureComponent { | |||
} | |||
render () { | |||
const { icon, items, size, title, disabled } = this.props; | |||
const { placement } = this.state; | |||
const show = placement !== null; | |||
const { icon, items, size, title, disabled, dropdownPlacement, openDropdownId } = this.props; | |||
const open = this.state.id === openDropdownId; | |||
return ( | |||
<div onKeyDown={this.handleKeyDown}> | |||
<IconButton | |||
icon={icon} | |||
title={title} | |||
active={show} | |||
active={open} | |||
disabled={disabled} | |||
size={size} | |||
ref={this.setTargetRef} | |||
onClick={this.handleClick} | |||
/> | |||
<Overlay show={show} placement={placement} target={this.findTarget}> | |||
<Overlay show={open} placement={dropdownPlacement} target={this.findTarget}> | |||
<DropdownMenu items={items} onClose={this.handleClose} /> | |||
</Overlay> | |||
</div> | |||
@@ -1,3 +1,4 @@ | |||
import { openDropdownMenu, closeDropdownMenu } from '../actions/dropdown_menu'; | |||
import { openModal, closeModal } from '../actions/modal'; | |||
import { connect } from 'react-redux'; | |||
import DropdownMenu from '../components/dropdown_menu'; | |||
@@ -5,12 +6,22 @@ import { isUserTouching } from '../is_mobile'; | |||
const mapStateToProps = state => ({ | |||
isModalOpen: state.get('modal').modalType === 'ACTIONS', | |||
dropdownPlacement: state.getIn(['dropdown_menu', 'placement']), | |||
openDropdownId: state.getIn(['dropdown_menu', 'openId']), | |||
}); | |||
const mapDispatchToProps = dispatch => ({ | |||
isUserTouching, | |||
onModalOpen: props => dispatch(openModal('ACTIONS', props)), | |||
onModalClose: () => dispatch(closeModal()), | |||
const mapDispatchToProps = (dispatch, { status, items }) => ({ | |||
onOpen(id, onItemClick, dropdownPlacement) { | |||
dispatch(isUserTouching() ? openModal('ACTIONS', { | |||
status, | |||
actions: items, | |||
onClick: onItemClick, | |||
}) : openDropdownMenu(id, dropdownPlacement)); | |||
}, | |||
onClose(id) { | |||
dispatch(closeModal()); | |||
dispatch(closeDropdownMenu(id)); | |||
}, | |||
}); | |||
export default connect(mapStateToProps, mapDispatchToProps)(DropdownMenu); |
@@ -56,6 +56,7 @@ const messages = defineMessages({ | |||
const mapStateToProps = state => ({ | |||
isComposing: state.getIn(['compose', 'is_composing']), | |||
hasComposingText: state.getIn(['compose', 'text']) !== '', | |||
dropdownMenuIsOpen: state.getIn(['dropdown_menu', 'openId']) !== null, | |||
}); | |||
const keyMap = { | |||
@@ -184,6 +185,7 @@ export default class UI extends React.PureComponent { | |||
hasComposingText: PropTypes.bool, | |||
location: PropTypes.object, | |||
intl: PropTypes.object.isRequired, | |||
dropdownMenuIsOpen: PropTypes.bool, | |||
}; | |||
state = { | |||
@@ -405,7 +407,7 @@ export default class UI extends React.PureComponent { | |||
render () { | |||
const { draggingOver } = this.state; | |||
const { children, isComposing, location } = this.props; | |||
const { children, isComposing, location, dropdownMenuIsOpen } = this.props; | |||
const handlers = { | |||
help: this.handleHotkeyToggleHelp, | |||
@@ -428,7 +430,7 @@ export default class UI extends React.PureComponent { | |||
return ( | |||
<HotKeys keyMap={keyMap} handlers={handlers} ref={this.setHotkeysRef}> | |||
<div className={classNames('ui', { 'is-composing': isComposing })} ref={this.setRef}> | |||
<div className={classNames('ui', { 'is-composing': isComposing })} ref={this.setRef} style={{ pointerEvents: dropdownMenuIsOpen ? 'none' : null }}> | |||
<TabsBar /> | |||
<SwitchingColumnsArea location={location} onLayoutChange={this.handleLayoutChange}> | |||
@@ -0,0 +1,18 @@ | |||
import Immutable from 'immutable'; | |||
import { | |||
DROPDOWN_MENU_OPEN, | |||
DROPDOWN_MENU_CLOSE, | |||
} from '../actions/dropdown_menu'; | |||
const initialState = Immutable.Map({ openId: null, placement: null }); | |||
export default function dropdownMenu(state = initialState, action) { | |||
switch (action.type) { | |||
case DROPDOWN_MENU_OPEN: | |||
return state.merge({ openId: action.id, placement: action.placement }); | |||
case DROPDOWN_MENU_CLOSE: | |||
return state.get('openId') === action.id ? state.set('openId', null) : state; | |||
default: | |||
return state; | |||
} | |||
} |
@@ -1,4 +1,5 @@ | |||
import { combineReducers } from 'redux-immutable'; | |||
import dropdown_menu from './dropdown_menu'; | |||
import timelines from './timelines'; | |||
import meta from './meta'; | |||
import alerts from './alerts'; | |||
@@ -26,6 +27,7 @@ import lists from './lists'; | |||
import listEditor from './list_editor'; | |||
const reducers = { | |||
dropdown_menu, | |||
timelines, | |||
meta, | |||
alerts, | |||