diff options
Diffstat (limited to 'app')
-rw-r--r-- | app/javascript/mastodon/features/compose/components/privacy_dropdown.js | 154 | ||||
-rw-r--r-- | app/javascript/mastodon/features/compose/components/warning.js | 11 | ||||
-rw-r--r-- | app/javascript/styles/components.scss | 27 | ||||
-rw-r--r-- | app/javascript/styles/rtl.scss | 18 |
4 files changed, 138 insertions, 72 deletions
diff --git a/app/javascript/mastodon/features/compose/components/privacy_dropdown.js b/app/javascript/mastodon/features/compose/components/privacy_dropdown.js index 0474dfb4e..d5bb58712 100644 --- a/app/javascript/mastodon/features/compose/components/privacy_dropdown.js +++ b/app/javascript/mastodon/features/compose/components/privacy_dropdown.js @@ -2,7 +2,10 @@ import React from 'react'; import PropTypes from 'prop-types'; import { injectIntl, defineMessages } from 'react-intl'; import IconButton from '../../../components/icon_button'; +import { Overlay } from 'react-overlays'; +import { Motion, spring } from 'react-motion'; import detectPassiveEvents from 'detect-passive-events'; +import classNames from 'classnames'; const messages = defineMessages({ public_short: { id: 'privacy.public.short', defaultMessage: 'Public' }, @@ -16,10 +19,77 @@ const messages = defineMessages({ change_privacy: { id: 'privacy.change', defaultMessage: 'Adjust status privacy' }, }); -const iconStyle = { - height: null, - lineHeight: '27px', -}; +const listenerOptions = detectPassiveEvents.hasSupport ? { passive: true } : false; + +class PrivacyDropdownMenu extends React.PureComponent { + + static propTypes = { + style: PropTypes.object, + items: PropTypes.array.isRequired, + value: PropTypes.string.isRequired, + onClose: PropTypes.func.isRequired, + onChange: PropTypes.func.isRequired, + }; + + handleDocumentClick = e => { + if (this.node && !this.node.contains(e.target)) { + this.props.onClose(); + } + } + + handleClick = e => { + if (e.key === 'Escape') { + this.props.onClose(); + } else if (!e.key || e.key === 'Enter') { + const value = e.currentTarget.getAttribute('data-index'); + + e.preventDefault(); + + this.props.onClose(); + this.props.onChange(value); + } + } + + componentDidMount () { + document.addEventListener('click', this.handleDocumentClick, false); + document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); + } + + componentWillUnmount () { + document.removeEventListener('click', this.handleDocumentClick, false); + document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions); + } + + setRef = c => { + this.node = c; + } + + render () { + const { style, items, value } = this.props; + + 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 }) => ( + <div className='privacy-dropdown__dropdown' style={{ ...style, opacity: opacity, transform: `scale(${scaleX}, ${scaleY})` }} ref={this.setRef}> + {items.map(item => + <div role='button' tabIndex='0' key={item.value} data-index={item.value} onKeyDown={this.handleClick} onClick={this.handleClick} className={classNames('privacy-dropdown__option', { active: item.value === value })}> + <div className='privacy-dropdown__option__icon'> + <i className={`fa fa-fw fa-${item.icon}`} /> + </div> + + <div className='privacy-dropdown__option__content'> + <strong>{item.text}</strong> + {item.meta} + </div> + </div> + )} + </div> + )} + </Motion> + ); + } + +} @injectIntl export default class PrivacyDropdown extends React.PureComponent { @@ -55,26 +125,30 @@ export default class PrivacyDropdown extends React.PureComponent { handleModalActionClick = (e) => { e.preventDefault(); + const { value } = this.options[e.currentTarget.getAttribute('data-index')]; + this.props.onModalClose(); this.props.onChange(value); } - handleClick = (e) => { - if (e.key === 'Escape') { - this.setState({ open: false }); - } else if (!e.key || e.key === 'Enter') { - const value = e.currentTarget.getAttribute('data-index'); - e.preventDefault(); - this.setState({ open: false }); - this.props.onChange(value); + handleKeyDown = e => { + switch(e.key) { + case 'Enter': + this.handleToggle(); + break; + case 'Escape': + this.handleClose(); + break; } } - onGlobalClick = (e) => { - if (e.target !== this.node && !this.node.contains(e.target) && this.state.open) { - this.setState({ open: false }); - } + handleClose = () => { + this.setState({ open: false }); + } + + handleChange = value => { + this.props.onChange(value); } componentWillMount () { @@ -88,20 +162,6 @@ export default class PrivacyDropdown extends React.PureComponent { ]; } - componentDidMount () { - window.addEventListener('click', this.onGlobalClick); - window.addEventListener('touchstart', this.onGlobalClick, detectPassiveEvents.hasSupport ? { passive: true } : false); - } - - componentWillUnmount () { - window.removeEventListener('click', this.onGlobalClick); - window.removeEventListener('touchstart', this.onGlobalClick, detectPassiveEvents.hasSupport ? { passive: true } : false); - } - - setRef = (c) => { - this.node = c; - } - render () { const { value, intl } = this.props; const { open } = this.state; @@ -109,19 +169,29 @@ export default class PrivacyDropdown extends React.PureComponent { const valueOption = this.options.find(item => item.value === value); return ( - <div ref={this.setRef} className={`privacy-dropdown ${open ? 'active' : ''}`}> - <div className='privacy-dropdown__value'><IconButton className='privacy-dropdown__value-icon' icon={valueOption.icon} title={intl.formatMessage(messages.change_privacy)} size={18} expanded={open} active={open} inverted onClick={this.handleToggle} style={iconStyle} /></div> - <div className='privacy-dropdown__dropdown'> - {open && this.options.map(item => - <div role='button' tabIndex='0' key={item.value} data-index={item.value} onKeyDown={this.handleClick} onClick={this.handleClick} className={`privacy-dropdown__option ${item.value === value ? 'active' : ''}`}> - <div className='privacy-dropdown__option__icon'><i className={`fa fa-fw fa-${item.icon}`} /></div> - <div className='privacy-dropdown__option__content'> - <strong>{item.text}</strong> - {item.meta} - </div> - </div> - )} + <div className={classNames('privacy-dropdown', { active: open })} onKeyDown={this.handleKeyDown}> + <div className={classNames('privacy-dropdown__value', { active: this.options.indexOf(valueOption) === 0 })}> + <IconButton + className='privacy-dropdown__value-icon' + icon={valueOption.icon} + title={intl.formatMessage(messages.change_privacy)} + size={18} + expanded={open} + active={open} + inverted + onClick={this.handleToggle} + style={{ height: null, lineHeight: '27px' }} + /> </div> + + <Overlay show={open} placement='bottom' target={this}> + <PrivacyDropdownMenu + items={this.options} + value={value} + onClose={this.handleClose} + onChange={this.handleChange} + /> + </Overlay> </div> ); } diff --git a/app/javascript/mastodon/features/compose/components/warning.js b/app/javascript/mastodon/features/compose/components/warning.js index 75f36b840..dc902f33b 100644 --- a/app/javascript/mastodon/features/compose/components/warning.js +++ b/app/javascript/mastodon/features/compose/components/warning.js @@ -1,5 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; +import { Motion, spring } from 'react-motion'; export default class Warning extends React.PureComponent { @@ -11,9 +12,13 @@ export default class Warning extends React.PureComponent { const { message } = this.props; return ( - <div className='compose-form__warning'> - {message} - </div> + <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='compose-form__warning' style={{ opacity: opacity, transform: `scale(${scaleX}, ${scaleY})` }}> + {message} + </div> + )} + </Motion> ); } diff --git a/app/javascript/styles/components.scss b/app/javascript/styles/components.scss index 5ea0d134e..caa7c0787 100644 --- a/app/javascript/styles/components.scss +++ b/app/javascript/styles/components.scss @@ -1275,7 +1275,7 @@ background: $ui-secondary-color; padding: 4px 0; border-radius: 4px; - box-shadow: 0 0 15px rgba($base-shadow-color, 0.4); + box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4); ul { list-style: none; @@ -2805,19 +2805,12 @@ button.icon-button.active i.fa-retweet { filter: none; } -.privacy-dropdown { - position: relative; -} - .privacy-dropdown__dropdown { - display: none; position: absolute; - left: 0; - top: 27px; - width: 230px; background: $simple-background-color; - border-radius: 0 4px 4px; - z-index: 2; + box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4); + border-radius: 4px; + margin-left: 40px; overflow: hidden; } @@ -2869,6 +2862,18 @@ button.icon-button.active i.fa-retweet { background: $simple-background-color; border-radius: 4px 4px 0 0; box-shadow: 0 -4px 4px rgba($base-shadow-color, 0.1); + + .icon-button { + transition: none; + } + + &.active { + background: $ui-highlight-color; + + .icon-button { + color: $primary-text-color; + } + } } .privacy-dropdown__dropdown { diff --git a/app/javascript/styles/rtl.scss b/app/javascript/styles/rtl.scss index 0fdeccd9c..67bfa8a38 100644 --- a/app/javascript/styles/rtl.scss +++ b/app/javascript/styles/rtl.scss @@ -128,22 +128,8 @@ body.rtl { } .privacy-dropdown__dropdown { - left: auto; - right: 0; - } - - .dropdown--active .dropdown__content { - text-align: right; - } - - .dropdown--active .dropdown__content::before { - left: auto; - right: 8px; - } - - .dropdown--active .dropdown__content > ul { - left: auto; - right: -10px; + margin-left: 0; + margin-right: 40px; } .privacy-dropdown__option__icon { |