diff --git a/app/javascript/mastodon/features/compose/components/federation_dropdown.js b/app/javascript/mastodon/features/compose/components/federation_dropdown.jsx similarity index 51% rename from app/javascript/mastodon/features/compose/components/federation_dropdown.js rename to app/javascript/mastodon/features/compose/components/federation_dropdown.jsx index e1224dd93..69f6c6a3f 100644 --- a/app/javascript/mastodon/features/compose/components/federation_dropdown.js +++ b/app/javascript/mastodon/features/compose/components/federation_dropdown.jsx @@ -1,13 +1,17 @@ -import React from 'react'; import PropTypes from 'prop-types'; +import { PureComponent } from 'react'; + import { injectIntl, defineMessages } from 'react-intl'; -import IconButton from '../../../components/icon_button'; -import Overlay from 'react-overlays/lib/Overlay'; -import Motion from '../../ui/util/optional_motion'; -import spring from 'react-motion/lib/spring'; -import { supportsPassiveEvents } from 'detect-passive-events'; + import classNames from 'classnames'; +import { supportsPassiveEvents } from 'detect-passive-events'; +import Overlay from 'react-overlays/Overlay'; + +import { Icon } from 'mastodon/components/icon'; + +import { IconButton } from '../../../components/icon_button'; + const messages = defineMessages({ federate_short: { id: 'federation.federated.short', defaultMessage: 'Federated' }, federate_long: { id: 'federation.federated.long', defaultMessage: 'Allow post to reach other instances' }, @@ -16,37 +20,32 @@ const messages = defineMessages({ change_federation: { id: 'federation.change', defaultMessage: 'Adjust status federation' }, }); -const listenerOptions = supportsPassiveEvents ? { passive: true } : false; - -const getValue = element => element.dataset.index === 'true'; +const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true; -class FederationDropdownMenu extends React.PureComponent { +class FederationDropdownMenu extends PureComponent { static propTypes = { style: PropTypes.object, items: PropTypes.array.isRequired, - value: PropTypes.bool.isRequired, + value: PropTypes.string.isRequired, onClose: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired, }; - state = { - mounted: false, - }; - handleDocumentClick = e => { if (this.node && !this.node.contains(e.target)) { this.props.onClose(); + e.stopPropagation(); } - } + }; handleKeyDown = e => { const { items } = this.props; - const value = getValue(e.currentTarget); + const value = e.currentTarget.getAttribute('data-index'); const index = items.findIndex(item => { return (item.value === value); }); - let element; + let element = null; switch(e.key) { case 'Escape': @@ -56,115 +55,106 @@ class FederationDropdownMenu extends React.PureComponent { this.handleClick(e); break; case 'ArrowDown': - element = this.node.childNodes[index + 1]; - if (element) { - element.focus(); - this.props.onChange(getValue(element)); - } + element = this.node.childNodes[index + 1] || this.node.firstChild; break; case 'ArrowUp': - element = this.node.childNodes[index - 1]; - if (element) { - element.focus(); - this.props.onChange(getValue(element)); + element = this.node.childNodes[index - 1] || this.node.lastChild; + break; + case 'Tab': + if (e.shiftKey) { + element = this.node.childNodes[index - 1] || this.node.lastChild; + } else { + element = this.node.childNodes[index + 1] || this.node.firstChild; } break; case 'Home': element = this.node.firstChild; - if (element) { - element.focus(); - this.props.onChange(getValue(element)); - } break; case 'End': element = this.node.lastChild; - if (element) { - element.focus(); - this.props.onChange(getValue(element)); - } break; } - } + + if (element) { + element.focus(); + this.props.onChange(element.getAttribute('data-index')); + e.preventDefault(); + e.stopPropagation(); + } + }; handleClick = e => { + const value = e.currentTarget.getAttribute('data-index'); + e.preventDefault(); this.props.onClose(); - this.props.onChange(getValue(e.currentTarget)); - } + this.props.onChange(value); + }; componentDidMount () { - document.addEventListener('click', this.handleDocumentClick, false); + document.addEventListener('click', this.handleDocumentClick, { capture: true }); document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); - if (this.focusedItem) this.focusedItem.focus(); - this.setState({ mounted: true }); + if (this.focusedItem) this.focusedItem.focus({ preventScroll: true }); } componentWillUnmount () { - document.removeEventListener('click', this.handleDocumentClick, false); + document.removeEventListener('click', this.handleDocumentClick, { capture: true }); document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions); } setRef = c => { this.node = c; - } + }; setFocusRef = c => { this.focusedItem = c; - } + }; render () { - const { mounted } = this.state; const { style, items, value } = this.props; 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 -
- {items.map(item => ( -
-
- -
- -
- {item.text} - {item.meta} -
-
- ))} +
+ {items.map(item => ( +
+
+ +
+ +
+ {item.text} + {item.meta} +
- )} - + ))} +
); } } -@injectIntl -export default class FederationDropdown extends React.PureComponent { +class FederationDropdown extends PureComponent { static propTypes = { isUserTouching: PropTypes.func, - isModalOpen: PropTypes.bool.isRequired, onModalOpen: PropTypes.func, onModalClose: PropTypes.func, value: PropTypes.bool.isRequired, onChange: PropTypes.func.isRequired, + noDirect: PropTypes.bool, + container: PropTypes.func, disabled: PropTypes.bool, intl: PropTypes.object.isRequired, }; state = { open: false, - placement: null, + placement: 'bottom', }; - handleToggle = ({ target }) => { - if (this.props.isUserTouching()) { + handleToggle = () => { + if (this.props.isUserTouching && this.props.isUserTouching()) { if (this.state.open) { this.props.onModalClose(); } else { @@ -174,11 +164,12 @@ export default class FederationDropdown extends React.PureComponent { }); } } else { - const { top } = target.getBoundingClientRect(); - this.setState({ placement: top * 2 < innerHeight ? 'bottom' : 'top' }); + if (this.state.open && this.activeElement) { + this.activeElement.focus({ preventScroll: true }); + } this.setState({ open: !this.state.open }); } - } + }; handleModalActionClick = (e) => { e.preventDefault(); @@ -187,7 +178,7 @@ export default class FederationDropdown extends React.PureComponent { this.props.onModalClose(); this.props.onChange(value); - } + }; handleKeyDown = e => { switch(e.key) { @@ -195,17 +186,38 @@ export default class FederationDropdown extends React.PureComponent { this.handleClose(); break; } - } + }; + + handleMouseDown = () => { + if (!this.state.open) { + this.activeElement = document.activeElement; + } + }; + + handleButtonKeyDown = (e) => { + switch(e.key) { + case ' ': + case 'Enter': + this.handleMouseDown(); + break; + } + }; handleClose = () => { + if (this.state.open && this.activeElement) { + this.activeElement.focus({ preventScroll: true }); + } this.setState({ open: false }); - } + }; handleChange = value => { + // handleChange receives the values as string, therefore we need to convert them + // to proper JS booleans. + value = value === "true"; this.props.onChange(value); - } + }; - componentWillMount () { + UNSAFE_componentWillMount () { const { intl: { formatMessage } } = this.props; this.options = [ @@ -214,15 +226,27 @@ export default class FederationDropdown extends React.PureComponent { ]; } + setTargetRef = c => { + this.target = c; + }; + + findTarget = () => { + return this.target; + }; + + handleOverlayEnter = (state) => { + this.setState({ placement: state.placement }); + }; + render () { - const { value, intl, disabled } = this.props; + const { value, container, disabled, intl } = this.props; const { open, placement } = this.state; const valueOption = this.options.find(item => item.value === value); return ( -
-
+
+
- - + + {({ props, placement }) => ( +
+
+ +
+
+ )}
); } } + +export default injectIntl(FederationDropdown); diff --git a/app/javascript/mastodon/features/compose/containers/federation_dropdown_container.js b/app/javascript/mastodon/features/compose/containers/federation_dropdown_container.js index bd519f288..04eeb80c8 100644 --- a/app/javascript/mastodon/features/compose/containers/federation_dropdown_container.js +++ b/app/javascript/mastodon/features/compose/containers/federation_dropdown_container.js @@ -6,7 +6,6 @@ import { isUserTouching } from '../../../is_mobile'; import FederationDropdown from '../components/federation_dropdown'; const mapStateToProps = state => ({ - isModalOpen: state.get('modal').modalType === 'ACTIONS', value: state.getIn(['compose', 'federation']), }); @@ -17,9 +16,14 @@ const mapDispatchToProps = dispatch => ({ }, isUserTouching, - onModalOpen: props => dispatch(openModal('ACTIONS', props)), - onModalClose: () => dispatch(closeModal()), - + onModalOpen: props => dispatch(openModal({ + modalType: 'ACTIONS', + modalProps: props, + })), + onModalClose: () => dispatch(closeModal({ + modalType: undefined, + ignoreFocus: false, + })), }); export default connect(mapStateToProps, mapDispatchToProps)(FederationDropdown); diff --git a/app/javascript/mastodon/features/ui/components/column_link.jsx b/app/javascript/mastodon/features/ui/components/column_link.jsx index 1d6fe36bd..fb86799a2 100644 --- a/app/javascript/mastodon/features/ui/components/column_link.jsx +++ b/app/javascript/mastodon/features/ui/components/column_link.jsx @@ -46,7 +46,7 @@ ColumnLink.propTypes = { badge: PropTypes.node, transparent: PropTypes.bool, button: PropTypes.bool, - onClick: PropTypes.function, + onClick: PropTypes.func, }; export default ColumnLink;