about summary refs log tree commit diff
diff options
context:
space:
mode:
authorEugen Rochko <eugen@zeonfederated.com>2017-10-01 12:20:00 +0200
committerGitHub <noreply@github.com>2017-10-01 12:20:00 +0200
commitcdad7977fc94cd6a1a97841ed0f25e8504cb80d6 (patch)
treeba164d7ea8a1528708beda57f3fe13dc7334ef10
parent0b3f1ec62a08ab2aad2b7c1ab8f88bdac5e8a3c6 (diff)
Improve privacy dropdown, remove react-simple-dropdown dependency (#5140)
* Improve privacy dropdown, remove react-simple-dropdown dependency

* Animate privacy warning

* Fix react-router-scroll
-rw-r--r--app/javascript/mastodon/features/compose/components/privacy_dropdown.js154
-rw-r--r--app/javascript/mastodon/features/compose/components/warning.js11
-rw-r--r--app/javascript/styles/components.scss27
-rw-r--r--app/javascript/styles/rtl.scss18
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 {