From 2871a82fccf2b60ca25379f0557d457fae5df534 Mon Sep 17 00:00:00 2001 From: Thibaut Girka Date: Fri, 30 Mar 2018 12:31:29 +0200 Subject: [PATCH 1/2] [Glitch] Place dropdown menu top if it is closer to the bottom of the viewport Port f6a8d835d30f61628ca731462ecfe4f6005e6a9e to glitch-soc --- .../glitch/components/dropdown_menu.js | 35 ++++++++++++------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/app/javascript/flavours/glitch/components/dropdown_menu.js b/app/javascript/flavours/glitch/components/dropdown_menu.js index 7ba7fb22be8..717b3a177c4 100644 --- a/app/javascript/flavours/glitch/components/dropdown_menu.js +++ b/app/javascript/flavours/glitch/components/dropdown_menu.js @@ -29,6 +29,10 @@ class DropdownMenu extends React.PureComponent { placement: 'bottom', }; + state = { + mounted: false, + }; + handleDocumentClick = e => { if (this.node && !this.node.contains(e.target)) { this.props.onClose(); @@ -38,6 +42,7 @@ class DropdownMenu extends React.PureComponent { componentDidMount () { document.addEventListener('click', this.handleDocumentClick, false); document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); + this.setState({ mounted: true }); } componentWillUnmount () { @@ -82,11 +87,15 @@ class DropdownMenu extends React.PureComponent { render () { const { items, style, placement, arrowOffsetLeft, arrowOffsetTop } = this.props; + const { mounted } = this.state; return ( {({ opacity, scaleX, scaleY }) => ( -
+ // It should not be transformed when mounting because the resulting + // size will be used to determine the coordinate of the menu by + // react-overlays +
    @@ -124,11 +133,13 @@ export default class Dropdown extends React.PureComponent { }; state = { - expanded: false, + placement: null, }; - handleClick = () => { - if (!this.state.expanded && this.props.isUserTouching() && this.props.onModalOpen) { + 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({ @@ -141,11 +152,10 @@ export default class Dropdown extends React.PureComponent { } : null ), }); - - return; + } else { + const { top } = target.getBoundingClientRect(); + this.setState({ placement: top * 2 < innerHeight ? 'bottom' : 'top' }); } - - this.setState({ expanded: !this.state.expanded }); } handleClose = () => { @@ -153,7 +163,7 @@ export default class Dropdown extends React.PureComponent { this.props.onModalClose(); } - this.setState({ expanded: false }); + this.setState({ placement: null }); } handleKeyDown = e => { @@ -191,21 +201,22 @@ export default class Dropdown extends React.PureComponent { render () { const { icon, items, size, ariaLabel, disabled } = this.props; - const { expanded } = this.state; + const { placement } = this.state; + const show = placement !== null; return (
    - +
    From 48bcf4d6e89658abac5405683e823c68f27248f0 Mon Sep 17 00:00:00 2001 From: Thibaut Girka Date: Fri, 30 Mar 2018 12:45:23 +0200 Subject: [PATCH 2/2] [Glitch] Remove pointer events on the entire UI when a dropdown menu is open Port 913a38111ff5b0cb7f412bec93e8314859c88855 to glitch-soc --- .../flavours/glitch/actions/dropdown_menu.js | 10 ++++ .../glitch/components/dropdown_menu.js | 47 +++++++------------ .../containers/dropdown_menu_container.js | 19 ++++++-- .../flavours/glitch/features/ui/index.js | 6 ++- .../flavours/glitch/reducers/dropdown_menu.js | 18 +++++++ .../flavours/glitch/reducers/index.js | 2 + 6 files changed, 66 insertions(+), 36 deletions(-) create mode 100644 app/javascript/flavours/glitch/actions/dropdown_menu.js create mode 100644 app/javascript/flavours/glitch/reducers/dropdown_menu.js diff --git a/app/javascript/flavours/glitch/actions/dropdown_menu.js b/app/javascript/flavours/glitch/actions/dropdown_menu.js new file mode 100644 index 00000000000..217ba4e74a5 --- /dev/null +++ b/app/javascript/flavours/glitch/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/flavours/glitch/components/dropdown_menu.js b/app/javascript/flavours/glitch/components/dropdown_menu.js index 717b3a177c4..245bebef331 100644 --- a/app/javascript/flavours/glitch/components/dropdown_menu.js +++ b/app/javascript/flavours/glitch/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,43 +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.map( - (item, i) => item ? { - ...item, - name: `${item.text}-${i}`, - onClick: this.handleItemClick.bind(this, i), - } : null - ), - }); + 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(); @@ -200,23 +188,22 @@ export default class Dropdown extends React.PureComponent { } render () { - const { icon, items, size, ariaLabel, disabled } = this.props; - const { placement } = this.state; - const show = placement !== null; + const { icon, items, size, ariaLabel, disabled, dropdownPlacement, openDropdownId } = this.props; + const open = this.state.id === openDropdownId; return (
    - +
    diff --git a/app/javascript/flavours/glitch/containers/dropdown_menu_container.js b/app/javascript/flavours/glitch/containers/dropdown_menu_container.js index 0b4f72fa163..9d490de172d 100644 --- a/app/javascript/flavours/glitch/containers/dropdown_menu_container.js +++ b/app/javascript/flavours/glitch/containers/dropdown_menu_container.js @@ -1,3 +1,4 @@ +import { openDropdownMenu, closeDropdownMenu } from 'flavours/glitch/actions/dropdown_menu'; import { openModal, closeModal } from 'flavours/glitch/actions/modal'; import { connect } from 'react-redux'; import DropdownMenu from 'flavours/glitch/components/dropdown_menu'; @@ -5,12 +6,22 @@ import { isUserTouching } from 'flavours/glitch/util/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/flavours/glitch/features/ui/index.js b/app/javascript/flavours/glitch/features/ui/index.js index 0b031a7f0e7..daf62092744 100644 --- a/app/javascript/flavours/glitch/features/ui/index.js +++ b/app/javascript/flavours/glitch/features/ui/index.js @@ -60,6 +60,7 @@ const mapStateToProps = state => ({ layout: state.getIn(['local_settings', 'layout']), isWide: state.getIn(['local_settings', 'stretch']), navbarUnder: state.getIn(['local_settings', 'navbar_under']), + dropdownMenuIsOpen: state.getIn(['dropdown_menu', 'openId']) !== null, }); const keyMap = { @@ -111,6 +112,7 @@ export default class UI extends React.Component { hasComposingText: PropTypes.bool, location: PropTypes.object, intl: PropTypes.object.isRequired, + dropdownMenuIsOpen: PropTypes.bool, }; state = { @@ -366,7 +368,7 @@ export default class UI extends React.Component { render () { const { width, draggingOver } = this.state; - const { children, layout, isWide, navbarUnder } = this.props; + const { children, layout, isWide, navbarUnder, dropdownMenuIsOpen } = this.props; const columnsClass = layout => { switch (layout) { @@ -407,7 +409,7 @@ export default class UI extends React.Component { return ( -
    +
    {navbarUnder ? null : ()} diff --git a/app/javascript/flavours/glitch/reducers/dropdown_menu.js b/app/javascript/flavours/glitch/reducers/dropdown_menu.js new file mode 100644 index 00000000000..5449884cc58 --- /dev/null +++ b/app/javascript/flavours/glitch/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/flavours/glitch/reducers/index.js b/app/javascript/flavours/glitch/reducers/index.js index e9c8d7c1df1..173f95cd2f7 100644 --- a/app/javascript/flavours/glitch/reducers/index.js +++ b/app/javascript/flavours/glitch/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'; @@ -27,6 +28,7 @@ import lists from './lists'; import listEditor from './list_editor'; const reducers = { + dropdown_menu, timelines, meta, alerts,