diff --git a/app/javascript/mastodon/actions/dropdown_menu.js b/app/javascript/mastodon/actions/dropdown_menu.js new file mode 100644 index 000000000..217ba4e74 --- /dev/null +++ b/app/javascript/mastodon/actions/dropdown_menu.js @@ -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 }; +} diff --git a/app/javascript/mastodon/components/dropdown_menu.js b/app/javascript/mastodon/components/dropdown_menu.js index 42c253449..c5c6f73b3 100644 --- a/app/javascript/mastodon/components/dropdown_menu.js +++ b/app/javascript/mastodon/components/dropdown_menu.js @@ -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 (
- +
diff --git a/app/javascript/mastodon/containers/dropdown_menu_container.js b/app/javascript/mastodon/containers/dropdown_menu_container.js index 151f25390..7cbcdcd35 100644 --- a/app/javascript/mastodon/containers/dropdown_menu_container.js +++ b/app/javascript/mastodon/containers/dropdown_menu_container.js @@ -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); diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js index 9960758af..6cf00222a 100644 --- a/app/javascript/mastodon/features/ui/index.js +++ b/app/javascript/mastodon/features/ui/index.js @@ -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 ( -
+
diff --git a/app/javascript/mastodon/reducers/dropdown_menu.js b/app/javascript/mastodon/reducers/dropdown_menu.js new file mode 100644 index 000000000..5449884cc --- /dev/null +++ b/app/javascript/mastodon/reducers/dropdown_menu.js @@ -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; + } +} diff --git a/app/javascript/mastodon/reducers/index.js b/app/javascript/mastodon/reducers/index.js index a028e989c..b84b2d18a 100644 --- a/app/javascript/mastodon/reducers/index.js +++ b/app/javascript/mastodon/reducers/index.js @@ -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,