diff options
14 files changed, 234 insertions, 256 deletions
diff --git a/app/javascript/flavours/glitch/actions/dropdown_menu.js b/app/javascript/flavours/glitch/actions/dropdown_menu.js index fb6e55612..023151d4b 100644 --- a/app/javascript/flavours/glitch/actions/dropdown_menu.js +++ b/app/javascript/flavours/glitch/actions/dropdown_menu.js @@ -1,8 +1,8 @@ export const DROPDOWN_MENU_OPEN = 'DROPDOWN_MENU_OPEN'; export const DROPDOWN_MENU_CLOSE = 'DROPDOWN_MENU_CLOSE'; -export function openDropdownMenu(id, placement, keyboard, scroll_key) { - return { type: DROPDOWN_MENU_OPEN, id, placement, keyboard, scroll_key }; +export function openDropdownMenu(id, keyboard, scroll_key) { + return { type: DROPDOWN_MENU_OPEN, id, keyboard, scroll_key }; } export function closeDropdownMenu(id) { diff --git a/app/javascript/flavours/glitch/components/dropdown_menu.js b/app/javascript/flavours/glitch/components/dropdown_menu.js index 036e0b909..7c70f750f 100644 --- a/app/javascript/flavours/glitch/components/dropdown_menu.js +++ b/app/javascript/flavours/glitch/components/dropdown_menu.js @@ -2,9 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import IconButton from './icon_button'; -import Overlay from 'react-overlays/lib/Overlay'; -import Motion from '../features/ui/util/optional_motion'; -import spring from 'react-motion/lib/spring'; +import Overlay from 'react-overlays/Overlay'; import { supportsPassiveEvents } from 'detect-passive-events'; import classNames from 'classnames'; import { CircularProgress } from 'flavours/glitch/components/loading_indicator'; @@ -24,9 +22,6 @@ class DropdownMenu extends React.PureComponent { scrollable: PropTypes.bool, onClose: PropTypes.func.isRequired, style: PropTypes.object, - placement: PropTypes.string, - arrowOffsetLeft: PropTypes.string, - arrowOffsetTop: PropTypes.string, openedViaKeyboard: PropTypes.bool, renderItem: PropTypes.func, renderHeader: PropTypes.func, @@ -35,11 +30,6 @@ class DropdownMenu extends React.PureComponent { static defaultProps = { style: {}, - placement: 'bottom', - }; - - state = { - mounted: false, }; handleDocumentClick = e => { @@ -56,8 +46,6 @@ class DropdownMenu extends React.PureComponent { if (this.focusedItem && this.props.openedViaKeyboard) { this.focusedItem.focus({ preventScroll: true }); } - - this.setState({ mounted: true }); } componentWillUnmount () { @@ -139,40 +127,28 @@ class DropdownMenu extends React.PureComponent { } render () { - const { items, style, placement, arrowOffsetLeft, arrowOffsetTop, scrollable, renderHeader, loading } = this.props; - const { mounted } = this.state; + const { items, scrollable, renderHeader, loading } = this.props; let renderItem = this.props.renderItem || this.renderItem; 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 }) }}> - {({ 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 - <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={classNames('dropdown-menu__container', { 'dropdown-menu__container--loading': loading })}> - {loading && ( - <CircularProgress size={30} strokeWidth={3.5} /> - )} - - {!loading && renderHeader && ( - <div className='dropdown-menu__container__header'> - {renderHeader(items)} - </div> - )} - - {!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 }))} - </ul> - )} - </div> + <div className={classNames('dropdown-menu__container', { 'dropdown-menu__container--loading': loading })} ref={this.setRef}> + {loading && ( + <CircularProgress size={30} strokeWidth={3.5} /> + )} + + {!loading && renderHeader && ( + <div className='dropdown-menu__container__header'> + {renderHeader(items)} </div> )} - </Motion> + + {!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 }))} + </ul> + )} + </div> ); } @@ -197,7 +173,6 @@ export default class Dropdown extends React.PureComponent { isUserTouching: PropTypes.func, onOpen: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired, - dropdownPlacement: PropTypes.string, openDropdownId: PropTypes.number, openedViaKeyboard: PropTypes.bool, renderItem: PropTypes.func, @@ -213,13 +188,11 @@ export default class Dropdown extends React.PureComponent { id: id++, }; - handleClick = ({ target, type }) => { + handleClick = ({ type }) => { if (this.state.id === this.props.openDropdownId) { this.handleClose(); } else { - const { top } = target.getBoundingClientRect(); - const placement = top * 2 < innerHeight ? 'bottom' : 'top'; - this.props.onOpen(this.state.id, this.handleItemClick, placement, type !== 'click'); + this.props.onOpen(this.state.id, this.handleItemClick, type !== 'click'); } } @@ -303,7 +276,6 @@ export default class Dropdown extends React.PureComponent { disabled, loading, scrollable, - dropdownPlacement, openDropdownId, openedViaKeyboard, children, @@ -314,7 +286,6 @@ export default class Dropdown extends React.PureComponent { 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, @@ -326,7 +297,6 @@ export default class Dropdown extends React.PureComponent { active={open} disabled={disabled} size={size} - ref={this.setTargetRef} onClick={this.handleClick} onMouseDown={this.handleMouseDown} onKeyDown={this.handleButtonKeyDown} @@ -336,19 +306,27 @@ export default class Dropdown extends React.PureComponent { return ( <React.Fragment> - {button} - - <Overlay show={open} placement={dropdownPlacement} target={this.findTarget}> - <DropdownMenu - items={items} - loading={loading} - scrollable={scrollable} - onClose={this.handleClose} - openedViaKeyboard={openedViaKeyboard} - renderItem={renderItem} - renderHeader={renderHeader} - onItemClick={this.handleItemClick} - /> + <span ref={this.setTargetRef}> + {button} + </span> + <Overlay show={open} offset={[5, 5]} placement={'bottom'} flip target={this.findTarget} popperConfig={{ strategy: 'fixed' }}> + {({ props, arrowProps, placement }) => ( + <div {...props}> + <div className={`dropdown-animation dropdown-menu ${placement}`}> + <div className={`dropdown-menu__arrow ${placement}`} {...arrowProps} /> + <DropdownMenu + items={items} + loading={loading} + scrollable={scrollable} + onClose={this.handleClose} + openedViaKeyboard={openedViaKeyboard} + renderItem={renderItem} + renderHeader={renderHeader} + onItemClick={this.handleItemClick} + /> + </div> + </div> + )} </Overlay> </React.Fragment> ); diff --git a/app/javascript/flavours/glitch/components/edited_timestamp/containers/dropdown_menu_container.js b/app/javascript/flavours/glitch/components/edited_timestamp/containers/dropdown_menu_container.js index 8b73663d4..a1519757d 100644 --- a/app/javascript/flavours/glitch/components/edited_timestamp/containers/dropdown_menu_container.js +++ b/app/javascript/flavours/glitch/components/edited_timestamp/containers/dropdown_menu_container.js @@ -4,7 +4,6 @@ import { fetchHistory } from 'flavours/glitch/actions/history'; import DropdownMenu from 'flavours/glitch/components/dropdown_menu'; const mapStateToProps = (state, { statusId }) => ({ - dropdownPlacement: state.getIn(['dropdown_menu', 'placement']), openDropdownId: state.getIn(['dropdown_menu', 'openId']), openedViaKeyboard: state.getIn(['dropdown_menu', 'keyboard']), items: state.getIn(['history', statusId, 'items']), @@ -13,9 +12,9 @@ const mapStateToProps = (state, { statusId }) => ({ const mapDispatchToProps = (dispatch, { statusId }) => ({ - onOpen (id, onItemClick, dropdownPlacement, keyboard) { + onOpen (id, onItemClick, keyboard) { dispatch(fetchHistory(statusId)); - dispatch(openDropdownMenu(id, dropdownPlacement, keyboard)); + dispatch(openDropdownMenu(id, keyboard)); }, onClose (id) { diff --git a/app/javascript/flavours/glitch/containers/dropdown_menu_container.js b/app/javascript/flavours/glitch/containers/dropdown_menu_container.js index b2dff63db..43ce8ca63 100644 --- a/app/javascript/flavours/glitch/containers/dropdown_menu_container.js +++ b/app/javascript/flavours/glitch/containers/dropdown_menu_container.js @@ -5,18 +5,17 @@ import DropdownMenu from 'flavours/glitch/components/dropdown_menu'; import { isUserTouching } from '../is_mobile'; const mapStateToProps = state => ({ - dropdownPlacement: state.getIn(['dropdown_menu', 'placement']), openDropdownId: state.getIn(['dropdown_menu', 'openId']), openedViaKeyboard: state.getIn(['dropdown_menu', 'keyboard']), }); const mapDispatchToProps = (dispatch, { status, items, scrollKey }) => ({ - onOpen(id, onItemClick, dropdownPlacement, keyboard) { + onOpen(id, onItemClick, keyboard) { dispatch(isUserTouching() ? openModal('ACTIONS', { status, actions: items, onClick: onItemClick, - }) : openDropdownMenu(id, dropdownPlacement, keyboard, scrollKey)); + }) : openDropdownMenu(id, keyboard, scrollKey)); }, onClose(id) { diff --git a/app/javascript/flavours/glitch/features/compose/components/dropdown.js b/app/javascript/flavours/glitch/features/compose/components/dropdown.js index 3de198c45..924618930 100644 --- a/app/javascript/flavours/glitch/features/compose/components/dropdown.js +++ b/app/javascript/flavours/glitch/features/compose/components/dropdown.js @@ -2,7 +2,7 @@ import classNames from 'classnames'; import PropTypes from 'prop-types'; import React from 'react'; -import Overlay from 'react-overlays/lib/Overlay'; +import Overlay from 'react-overlays/Overlay'; // Components. import IconButton from 'flavours/glitch/components/icon_button'; @@ -45,7 +45,7 @@ export default class ComposerOptionsDropdown extends React.PureComponent { }; // Toggles opening and closing the dropdown. - handleToggle = ({ target, type }) => { + handleToggle = ({ type }) => { const { onModalOpen } = this.props; const { open } = this.state; @@ -59,11 +59,9 @@ export default class ComposerOptionsDropdown extends React.PureComponent { } } } else { - const { top } = target.getBoundingClientRect(); if (this.state.open && this.activeElement) { this.activeElement.focus({ preventScroll: true }); } - this.setState({ placement: top * 2 < innerHeight ? 'bottom' : 'top' }); this.setState({ open: !this.state.open, openedViaKeyboard: type !== 'click' }); } } @@ -158,6 +156,18 @@ export default class ComposerOptionsDropdown extends React.PureComponent { }; } + setTargetRef = c => { + this.target = c; + } + + findTarget = () => { + return this.target; + } + + handleOverlayEnter = (state) => { + this.setState({ placement: state.placement }); + } + // Rendering. render () { const { @@ -179,6 +189,7 @@ export default class ComposerOptionsDropdown extends React.PureComponent { <div className={classNames('privacy-dropdown', placement, { active: open })} onKeyDown={this.handleKeyDown} + ref={this.setTargetRef} > <div className={classNames('privacy-dropdown__value', { active })}> <IconButton @@ -204,18 +215,26 @@ export default class ComposerOptionsDropdown extends React.PureComponent { containerPadding={20} placement={placement} show={open} - target={this} + flip + target={this.findTarget} container={container} + popperConfig={{ strategy: 'fixed', onFirstUpdate: this.handleOverlayEnter }} > - <DropdownMenu - items={items} - renderItemContents={renderItemContents} - onChange={onChange} - onClose={this.handleClose} - value={value} - openedViaKeyboard={this.state.openedViaKeyboard} - closeOnChange={closeOnChange} - /> + {({ props, placement }) => ( + <div {...props} style={{ ...props.style, width: 350, maxWidth: '100vw' }}> + <div className={`dropdown-animation privacy-dropdown__dropdown ${placement}`}> + <DropdownMenu + items={items} + renderItemContents={renderItemContents} + onChange={onChange} + onClose={this.handleClose} + value={value} + openedViaKeyboard={this.state.openedViaKeyboard} + closeOnChange={closeOnChange} + /> + </div> + </div> + )} </Overlay> </div> ); diff --git a/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.js b/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.js index 09e8fc35a..c4895dfd0 100644 --- a/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.js +++ b/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.js @@ -1,7 +1,6 @@ // Package imports. import PropTypes from 'prop-types'; import React from 'react'; -import spring from 'react-motion/lib/spring'; import ImmutablePureComponent from 'react-immutable-pure-component'; import classNames from 'classnames'; @@ -10,15 +9,8 @@ import Icon from 'flavours/glitch/components/icon'; // Utils. import { withPassive } from 'flavours/glitch/utils/dom_helpers'; -import Motion from '../../ui/util/optional_motion'; import { assignHandlers } from 'flavours/glitch/utils/react_helpers'; -// The spring to use with our motion. -const springMotion = spring(1, { - damping: 35, - stiffness: 400, -}); - // The component. export default class ComposerOptionsDropdownContent extends React.PureComponent { @@ -44,7 +36,6 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent }; state = { - mounted: false, value: this.props.openedViaKeyboard ? this.props.items[0].name : undefined, }; @@ -56,7 +47,7 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent } // Stores our node in `this.node`. - handleRef = (node) => { + setRef = (node) => { this.node = node; } @@ -69,7 +60,6 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent } else { this.node.firstChild.focus({ preventScroll: true }); } - this.setState({ mounted: true }); } // On unmounting, we remove our listeners. @@ -191,7 +181,6 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent // Rendering. render () { - const { mounted } = this.state; const { items, onChange, @@ -201,36 +190,9 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent // The result. return ( - <Motion - defaultStyle={{ - opacity: 0, - scaleX: 0.85, - scaleY: 0.75, - }} - style={{ - opacity: springMotion, - scaleX: springMotion, - scaleY: springMotion, - }} - > - {({ 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 - <div - className='privacy-dropdown__dropdown' - ref={this.handleRef} - role='listbox' - style={{ - ...style, - opacity: opacity, - transform: mounted ? `scale(${scaleX}, ${scaleY})` : null, - }} - > - {!!items && items.map((item, i) => this.renderItem(item, i))} - </div> - )} - </Motion> + <div style={{ ...style }} role='listbox' ref={this.setRef}> + {!!items && items.map((item, i) => this.renderItem(item, i))} + </div> ); } diff --git a/app/javascript/flavours/glitch/features/compose/components/emoji_picker_dropdown.js b/app/javascript/flavours/glitch/features/compose/components/emoji_picker_dropdown.js index 546d398a0..38c735551 100644 --- a/app/javascript/flavours/glitch/features/compose/components/emoji_picker_dropdown.js +++ b/app/javascript/flavours/glitch/features/compose/components/emoji_picker_dropdown.js @@ -2,7 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { EmojiPicker as EmojiPickerAsync } from '../../ui/util/async-components'; -import Overlay from 'react-overlays/lib/Overlay'; +import Overlay from 'react-overlays/Overlay'; import classNames from 'classnames'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { supportsPassiveEvents } from 'detect-passive-events'; @@ -155,9 +155,6 @@ class EmojiPickerMenu extends React.PureComponent { onClose: PropTypes.func.isRequired, onPick: PropTypes.func.isRequired, style: PropTypes.object, - placement: PropTypes.string, - arrowOffsetLeft: PropTypes.string, - arrowOffsetTop: PropTypes.string, intl: PropTypes.object.isRequired, skinTone: PropTypes.number.isRequired, onSkinTone: PropTypes.func.isRequired, @@ -326,14 +323,13 @@ class EmojiPickerDropdown extends React.PureComponent { state = { active: false, loading: false, - placement: null, }; setRef = (c) => { this.dropdown = c; } - onShowDropdown = ({ target }) => { + onShowDropdown = () => { this.setState({ active: true }); if (!EmojiPicker) { @@ -348,9 +344,6 @@ class EmojiPickerDropdown extends React.PureComponent { this.setState({ loading: false, active: false }); }); } - - const { top } = target.getBoundingClientRect(); - this.setState({ placement: top * 2 < innerHeight ? 'bottom' : 'top' }); } onHideDropdown = () => { @@ -384,7 +377,7 @@ class EmojiPickerDropdown extends React.PureComponent { render () { const { intl, onPickEmoji, onSkinTone, skinTone, frequentlyUsedEmojis, button } = this.props; const title = intl.formatMessage(messages.emoji); - const { active, loading, placement } = this.state; + const { active, loading } = this.state; return ( <div className='emoji-picker-dropdown' onKeyDown={this.handleKeyDown}> @@ -396,16 +389,22 @@ class EmojiPickerDropdown extends React.PureComponent { />} </div> - <Overlay show={active} placement={placement} target={this.findTarget}> - <EmojiPickerMenu - custom_emojis={this.props.custom_emojis} - loading={loading} - onClose={this.onHideDropdown} - onPick={onPickEmoji} - onSkinTone={onSkinTone} - skinTone={skinTone} - frequentlyUsedEmojis={frequentlyUsedEmojis} - /> + <Overlay show={active} placement={'bottom'} target={this.findTarget} popperConfig={{ strategy: 'fixed' }}> + {({ props, placement })=> ( + <div {...props} style={{ ...props.style, width: 299 }}> + <div className={`dropdown-animation ${placement}`}> + <EmojiPickerMenu + custom_emojis={this.props.custom_emojis} + loading={loading} + onClose={this.onHideDropdown} + onPick={onPickEmoji} + onSkinTone={onSkinTone} + skinTone={skinTone} + frequentlyUsedEmojis={frequentlyUsedEmojis} + /> + </div> + </div> + )} </Overlay> </div> ); diff --git a/app/javascript/flavours/glitch/features/compose/components/language_dropdown.js b/app/javascript/flavours/glitch/features/compose/components/language_dropdown.js index a3256aa9b..07d138f52 100644 --- a/app/javascript/flavours/glitch/features/compose/components/language_dropdown.js +++ b/app/javascript/flavours/glitch/features/compose/components/language_dropdown.js @@ -2,9 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { injectIntl, defineMessages } from 'react-intl'; import TextIconButton from './text_icon_button'; -import Overlay from 'react-overlays/lib/Overlay'; -import Motion from 'flavours/glitch/features/ui/util/optional_motion'; -import spring from 'react-motion/lib/spring'; +import Overlay from 'react-overlays/Overlay'; import { supportsPassiveEvents } from 'detect-passive-events'; import classNames from 'classnames'; import { languages as preloadedLanguages } from 'flavours/glitch/initial_state'; @@ -22,10 +20,8 @@ const listenerOptions = supportsPassiveEvents ? { passive: true } : false; class LanguageDropdownMenu extends React.PureComponent { static propTypes = { - style: PropTypes.object, value: PropTypes.string.isRequired, frequentlyUsedLanguages: PropTypes.arrayOf(PropTypes.string).isRequired, - placement: PropTypes.string.isRequired, onClose: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired, languages: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.string)), @@ -37,7 +33,6 @@ class LanguageDropdownMenu extends React.PureComponent { }; state = { - mounted: false, searchValue: '', }; @@ -50,7 +45,6 @@ class LanguageDropdownMenu extends React.PureComponent { componentDidMount () { document.addEventListener('click', this.handleDocumentClick, false); document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); - this.setState({ mounted: true }); // Because of https://github.com/react-bootstrap/react-bootstrap/issues/2614 we need // to wait for a frame before focusing @@ -222,29 +216,22 @@ class LanguageDropdownMenu extends React.PureComponent { } render () { - const { style, placement, intl } = this.props; - const { mounted, searchValue } = this.state; + const { intl } = this.props; + const { searchValue } = this.state; const isSearching = searchValue !== ''; const results = this.search(); 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 }) }}> - {({ 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 - <div className={`language-dropdown__dropdown ${placement}`} style={{ ...style, opacity: opacity, transform: mounted ? `scale(${scaleX}, ${scaleY})` : null }} ref={this.setRef}> - <div className='emoji-mart-search'> - <input type='search' value={searchValue} onChange={this.handleSearchChange} onKeyDown={this.handleSearchKeyDown} placeholder={intl.formatMessage(messages.search)} /> - <button className='emoji-mart-search-icon' disabled={!isSearching} aria-label={intl.formatMessage(messages.clear)} onClick={this.handleClear}>{!isSearching ? loupeIcon : deleteIcon}</button> - </div> + <div ref={this.setRef}> + <div className='emoji-mart-search'> + <input type='search' value={searchValue} onChange={this.handleSearchChange} onKeyDown={this.handleSearchKeyDown} placeholder={intl.formatMessage(messages.search)} /> + <button type='button' className='emoji-mart-search-icon' disabled={!isSearching} aria-label={intl.formatMessage(messages.clear)} onClick={this.handleClear}>{!isSearching ? loupeIcon : deleteIcon}</button> + </div> - <div className='language-dropdown__dropdown__results emoji-mart-scroll' role='listbox' ref={this.setListRef}> - {results.map(this.renderItem)} - </div> - </div> - )} - </Motion> + <div className='language-dropdown__dropdown__results emoji-mart-scroll' role='listbox' ref={this.setListRef}> + {results.map(this.renderItem)} + </div> + </div> ); } @@ -266,14 +253,11 @@ class LanguageDropdown extends React.PureComponent { placement: 'bottom', }; - handleToggle = ({ target }) => { - const { top } = target.getBoundingClientRect(); - + handleToggle = () => { if (this.state.open && this.activeElement) { this.activeElement.focus({ preventScroll: true }); } - this.setState({ placement: top * 2 < innerHeight ? 'bottom' : 'top' }); this.setState({ open: !this.state.open }); } @@ -293,13 +277,25 @@ class LanguageDropdown extends React.PureComponent { onChange(value); } + setTargetRef = c => { + this.target = c; + } + + findTarget = () => { + return this.target; + } + + handleOverlayEnter = (state) => { + this.setState({ placement: state.placement }); + } + render () { const { value, intl, frequentlyUsedLanguages } = this.props; const { open, placement } = this.state; return ( - <div className={classNames('privacy-dropdown', { active: open })}> - <div className='privacy-dropdown__value'> + <div className={classNames('privacy-dropdown', placement, { active: open })}> + <div className='privacy-dropdown__value' ref={this.setTargetRef} > <TextIconButton className='privacy-dropdown__value-icon' label={value && value.toUpperCase()} @@ -309,15 +305,20 @@ class LanguageDropdown extends React.PureComponent { /> </div> - <Overlay show={open} placement={placement} target={this}> - <LanguageDropdownMenu - value={value} - frequentlyUsedLanguages={frequentlyUsedLanguages} - onClose={this.handleClose} - onChange={this.handleChange} - placement={placement} - intl={intl} - /> + <Overlay show={open} placement={'bottom'} flip target={this.findTarget} popperConfig={{ strategy: 'fixed', onFirstUpdate: this.handleOverlayEnter }}> + {({ props, placement }) => ( + <div {...props} style={{ ...props.style, width: 280 }}> + <div className={`dropdown-animation language-dropdown__dropdown ${placement}`} > + <LanguageDropdownMenu + value={value} + frequentlyUsedLanguages={frequentlyUsedLanguages} + onClose={this.handleClose} + onChange={this.handleChange} + intl={intl} + /> + </div> + </div> + )} </Overlay> </div> ); diff --git a/app/javascript/flavours/glitch/features/compose/components/search.js b/app/javascript/flavours/glitch/features/compose/components/search.js index 9f90a767d..e5874de75 100644 --- a/app/javascript/flavours/glitch/features/compose/components/search.js +++ b/app/javascript/flavours/glitch/features/compose/components/search.js @@ -3,13 +3,12 @@ import classNames from 'classnames'; import PropTypes from 'prop-types'; import React from 'react'; import { connect } from 'react-redux'; -import spring from 'react-motion/lib/spring'; import { injectIntl, FormattedMessage, defineMessages, } from 'react-intl'; -import Overlay from 'react-overlays/lib/Overlay'; +import Overlay from 'react-overlays/Overlay'; // Components. import Icon from 'flavours/glitch/components/icon'; @@ -17,7 +16,6 @@ import Icon from 'flavours/glitch/components/icon'; // Utils. import { focusRoot } from 'flavours/glitch/utils/dom_helpers'; import { searchEnabled } from 'flavours/glitch/initial_state'; -import Motion from '../../ui/util/optional_motion'; const messages = defineMessages({ placeholder: { id: 'search.placeholder', defaultMessage: 'Search' }, @@ -26,31 +24,20 @@ const messages = defineMessages({ class SearchPopout extends React.PureComponent { - static propTypes = { - style: PropTypes.object, - }; - render () { - const { style } = this.props; const extraInformation = searchEnabled ? <FormattedMessage id='search_popout.tips.full_text' defaultMessage='Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.' /> : <FormattedMessage id='search_popout.tips.text' defaultMessage='Simple text returns matching display names, usernames and hashtags' />; return ( - <div style={{ ...style, position: 'absolute', width: 285, zIndex: 2 }}> - <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 }) => ( - <div className='search-popout' style={{ opacity: opacity, transform: `scale(${scaleX}, ${scaleY})` }}> - <h4><FormattedMessage id='search_popout.search_format' defaultMessage='Advanced search format' /></h4> - - <ul> - <li><em>#example</em> <FormattedMessage id='search_popout.tips.hashtag' defaultMessage='hashtag' /></li> - <li><em>@username@domain</em> <FormattedMessage id='search_popout.tips.user' defaultMessage='user' /></li> - <li><em>URL</em> <FormattedMessage id='search_popout.tips.user' defaultMessage='user' /></li> - <li><em>URL</em> <FormattedMessage id='search_popout.tips.status' defaultMessage='status' /></li> - </ul> - - {extraInformation} - </div> - )} - </Motion> + <div className='search-popout'> + <h4><FormattedMessage id='search_popout.search_format' defaultMessage='Advanced search format' /></h4> + + <ul> + <li><em>#example</em> <FormattedMessage id='search_popout.tips.hashtag' defaultMessage='hashtag' /></li> + <li><em>@username@domain</em> <FormattedMessage id='search_popout.tips.user' defaultMessage='user' /></li> + <li><em>URL</em> <FormattedMessage id='search_popout.tips.user' defaultMessage='user' /></li> + <li><em>URL</em> <FormattedMessage id='search_popout.tips.status' defaultMessage='status' /></li> + </ul> + + {extraInformation} </div> ); } @@ -136,6 +123,10 @@ class Search extends React.PureComponent { } } + findTarget = () => { + return this.searchForm; + } + render () { const { intl, value, submitted } = this.props; const { expanded } = this.state; @@ -161,8 +152,14 @@ class Search extends React.PureComponent { <Icon id='search' className={hasValue ? '' : 'active'} /> <Icon id='times-circle' className={hasValue ? 'active' : ''} /> </div> - <Overlay show={expanded && !hasValue} placement='bottom' target={this} container={this}> - <SearchPopout /> + <Overlay show={expanded && !hasValue} placement='bottom' target={this.findTarget} popperConfig={{ strategy: 'fixed' }}> + {({ props, placement }) => ( + <div {...props} style={{ ...props.style, width: 285, zIndex: 2 }}> + <div className={`dropdown-animation ${placement}`}> + <SearchPopout /> + </div> + </div> + )} </Overlay> </div> ); diff --git a/app/javascript/flavours/glitch/reducers/dropdown_menu.js b/app/javascript/flavours/glitch/reducers/dropdown_menu.js index a78a11acc..51bf9375b 100644 --- a/app/javascript/flavours/glitch/reducers/dropdown_menu.js +++ b/app/javascript/flavours/glitch/reducers/dropdown_menu.js @@ -4,12 +4,12 @@ import { DROPDOWN_MENU_CLOSE, } from '../actions/dropdown_menu'; -const initialState = Immutable.Map({ openId: null, placement: null, keyboard: false, scroll_key: null }); +const initialState = Immutable.Map({ openId: null, keyboard: false, scroll_key: null }); export default function dropdownMenu(state = initialState, action) { switch (action.type) { case DROPDOWN_MENU_OPEN: - return state.merge({ openId: action.id, placement: action.placement, keyboard: action.keyboard, scroll_key: action.scroll_key }); + return state.merge({ openId: action.id, keyboard: action.keyboard, scroll_key: action.scroll_key }); case DROPDOWN_MENU_CLOSE: return state.get('openId') === action.id ? state.set('openId', null).set('scroll_key', null) : state; default: diff --git a/app/javascript/flavours/glitch/styles/components/compose_form.scss b/app/javascript/flavours/glitch/styles/components/compose_form.scss index 72d3aad1d..aa2d52ed0 100644 --- a/app/javascript/flavours/glitch/styles/components/compose_form.scss +++ b/app/javascript/flavours/glitch/styles/components/compose_form.scss @@ -586,7 +586,6 @@ } .privacy-dropdown__dropdown { - position: absolute; border-radius: 4px; box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4); background: $simple-background-color; @@ -653,7 +652,6 @@ .language-dropdown { &__dropdown { - position: absolute; background: $simple-background-color; box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4); border-radius: 4px; diff --git a/app/javascript/flavours/glitch/styles/components/index.scss b/app/javascript/flavours/glitch/styles/components/index.scss index b7a54cd2b..d50316366 100644 --- a/app/javascript/flavours/glitch/styles/components/index.scss +++ b/app/javascript/flavours/glitch/styles/components/index.scss @@ -346,9 +346,8 @@ } } -.dropdown-menu { - position: absolute; - transform-origin: 50% 0; +body > [data-popper-placement] { + z-index: 3; } .invisible { @@ -532,6 +531,42 @@ } } +.dropdown-animation { + animation: dropdown 300ms cubic-bezier(0.1, 0.7, 0.1, 1); + + @keyframes dropdown { + from { + opacity: 0; + transform: scaleX(0.85) scaleY(0.75); + } + + to { + opacity: 1; + transform: scaleX(1) scaleY(1); + } + } + + &.top { + transform-origin: bottom; + } + + &.right { + transform-origin: left; + } + + &.bottom { + transform-origin: top; + } + + &.left { + transform-origin: right; + } + + .reduce-motion & { + animation: none; + } +} + .dropdown { display: inline-block; } @@ -600,36 +635,42 @@ .dropdown-menu__arrow { position: absolute; - width: 0; - height: 0; - border: 0 solid transparent; - &.left { - right: -5px; - margin-top: -5px; - border-width: 5px 0 5px 5px; - border-left-color: $ui-secondary-color; + &::before { + content: ''; + display: block; + width: 14px; + height: 5px; + background-color: $ui-secondary-color; + mask-image: url("data:image/svg+xml;utf8,<svg width='14' height='5' xmlns='http://www.w3.org/2000/svg'><path d='M7 0L0 5h14L7 0z' fill='white'/></svg>"); } &.top { bottom: -5px; - margin-left: -7px; - border-width: 5px 7px 0; - border-top-color: $ui-secondary-color; + + &::before { + transform: rotate(180deg); + } + } + + &.right { + left: -9px; + + &::before { + transform: rotate(-90deg); + } } &.bottom { top: -5px; - margin-left: -7px; - border-width: 0 7px 5px; - border-bottom-color: $ui-secondary-color; } - &.right { - left: -5px; - margin-top: -5px; - border-width: 5px 5px 5px 0; - border-right-color: $ui-secondary-color; + &.left { + right: -9px; + + &::before { + transform: rotate(90deg); + } } } diff --git a/app/javascript/flavours/glitch/styles/components/modal.scss b/app/javascript/flavours/glitch/styles/components/modal.scss index 8ba8bec10..972e01e7d 100644 --- a/app/javascript/flavours/glitch/styles/components/modal.scss +++ b/app/javascript/flavours/glitch/styles/components/modal.scss @@ -37,7 +37,6 @@ .modal-root__modal { pointer-events: auto; display: flex; - z-index: 9999; } .media-modal__zoom-button { diff --git a/app/javascript/flavours/glitch/styles/mastodon-light/diff.scss b/app/javascript/flavours/glitch/styles/mastodon-light/diff.scss index 9fc1aed2a..2ec2da833 100644 --- a/app/javascript/flavours/glitch/styles/mastodon-light/diff.scss +++ b/app/javascript/flavours/glitch/styles/mastodon-light/diff.scss @@ -285,22 +285,8 @@ html { .dropdown-menu { background: $white; - &__arrow { - &.left { - border-left-color: $white; - } - - &.top { - border-top-color: $white; - } - - &.bottom { - border-bottom-color: $white; - } - - &.right { - border-right-color: $white; - } + &__arrow::before { + background-color: $white; } &__item { |