about summary refs log tree commit diff
path: root/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.jsx
diff options
context:
space:
mode:
Diffstat (limited to 'app/javascript/flavours/glitch/features/compose/components/dropdown_menu.jsx')
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/dropdown_menu.jsx199
1 files changed, 199 insertions, 0 deletions
diff --git a/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.jsx b/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.jsx
new file mode 100644
index 000000000..1ccccad31
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.jsx
@@ -0,0 +1,199 @@
+//  Package imports.
+import PropTypes from 'prop-types';
+import React from 'react';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import classNames from 'classnames';
+
+//  Components.
+import Icon from 'flavours/glitch/components/icon';
+
+//  Utils.
+import { withPassive } from 'flavours/glitch/utils/dom_helpers';
+import { assignHandlers } from 'flavours/glitch/utils/react_helpers';
+
+//  The component.
+export default class ComposerOptionsDropdownContent extends React.PureComponent {
+
+  static propTypes = {
+    items: PropTypes.arrayOf(PropTypes.shape({
+      icon: PropTypes.string,
+      meta: PropTypes.node,
+      name: PropTypes.string.isRequired,
+      text: PropTypes.node,
+    })),
+    onChange: PropTypes.func.isRequired,
+    onClose: PropTypes.func.isRequired,
+    style: PropTypes.object,
+    value: PropTypes.string,
+    renderItemContents: PropTypes.func,
+    openedViaKeyboard: PropTypes.bool,
+    closeOnChange: PropTypes.bool,
+  };
+
+  static defaultProps = {
+    style: {},
+    closeOnChange: true,
+  };
+
+  state = {
+    value: this.props.openedViaKeyboard ? this.props.items[0].name : undefined,
+  };
+
+  //  When the document is clicked elsewhere, we close the dropdown.
+  handleDocumentClick = (e) => {
+    if (this.node && !this.node.contains(e.target)) {
+      this.props.onClose();
+    }
+  };
+
+  //  Stores our node in `this.node`.
+  setRef = (node) => {
+    this.node = node;
+  };
+
+  //  On mounting, we add our listeners.
+  componentDidMount () {
+    document.addEventListener('click', this.handleDocumentClick, false);
+    document.addEventListener('touchend', this.handleDocumentClick, withPassive);
+    if (this.focusedItem) {
+      this.focusedItem.focus({ preventScroll: true });
+    } else {
+      this.node.firstChild.focus({ preventScroll: true });
+    }
+  }
+
+  //  On unmounting, we remove our listeners.
+  componentWillUnmount () {
+    document.removeEventListener('click', this.handleDocumentClick, false);
+    document.removeEventListener('touchend', this.handleDocumentClick, withPassive);
+  }
+
+  handleClick = (e) => {
+    const i = Number(e.currentTarget.getAttribute('data-index'));
+
+    const {
+      onChange,
+      onClose,
+      closeOnChange,
+      items,
+    } = this.props;
+
+    const { name } = this.props.items[i];
+    e.preventDefault();  //  Prevents change in focus on click
+    if (closeOnChange) {
+      onClose();
+    }
+    onChange(name);
+  };
+
+  // Handle changes differently whether the dropdown is a list of options or actions
+  handleChange = (name) => {
+    if (this.props.value) {
+      this.props.onChange(name);
+    } else {
+      this.setState({ value: name });
+    }
+  };
+
+  handleKeyDown = (e) => {
+    const index = Number(e.currentTarget.getAttribute('data-index'));
+    const { items } = this.props;
+    let element = null;
+
+    switch(e.key) {
+    case 'Escape':
+      this.props.onClose();
+      break;
+    case 'Enter':
+    case ' ':
+      this.handleClick(e);
+      break;
+    case 'ArrowDown':
+      element = this.node.childNodes[index + 1] || this.node.firstChild;
+      break;
+    case 'ArrowUp':
+      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;
+      break;
+    case 'End':
+      element = this.node.lastChild;
+      break;
+    }
+
+    if (element) {
+      element.focus();
+      this.handleChange(this.props.items[Number(element.getAttribute('data-index'))].name);
+      e.preventDefault();
+      e.stopPropagation();
+    }
+  };
+
+  setFocusRef = c => {
+    this.focusedItem = c;
+  };
+
+  renderItem = (item, i) => {
+    const { name, icon, meta, text } = item;
+
+    const active = (name === (this.props.value || this.state.value));
+
+    const computedClass = classNames('privacy-dropdown__option', { active });
+
+    let contents = this.props.renderItemContents && this.props.renderItemContents(item, i);
+
+    if (!contents) {
+      contents = (
+        <React.Fragment>
+          {icon && <Icon className='icon' fixedWidth id={icon} />}
+
+          <div className='privacy-dropdown__option__content'>
+            <strong>{text}</strong>
+            {meta}
+          </div>
+        </React.Fragment>
+      );
+    }
+
+    return (
+      <div
+        className={computedClass}
+        onClick={this.handleClick}
+        onKeyDown={this.handleKeyDown}
+        role='option'
+        tabIndex={0}
+        key={name}
+        data-index={i}
+        ref={active ? this.setFocusRef : null}
+      >
+        {contents}
+      </div>
+    );
+  };
+
+  //  Rendering.
+  render () {
+    const {
+      items,
+      onChange,
+      onClose,
+      style,
+    } = this.props;
+
+    //  The result.
+    return (
+      <div style={{ ...style }} role='listbox' ref={this.setRef}>
+        {!!items && items.map((item, i) => this.renderItem(item, i))}
+      </div>
+    );
+  }
+
+}