about summary refs log tree commit diff
path: root/app/javascript/mastodon/features/compose/components/privacy_dropdown.js
diff options
context:
space:
mode:
authorThibG <thib@sitedethib.com>2018-05-04 22:13:26 +0200
committerEugen Rochko <eugen@zeonfederated.com>2018-05-04 22:13:26 +0200
commitef7d64c80109074b39983b50cc8cf701c337cdcc (patch)
tree57b3229696eb7e2fd40e04ecafee38b3d300c455 /app/javascript/mastodon/features/compose/components/privacy_dropdown.js
parent6793bec4c67e695100cb4d8551f0bda0b7e87b12 (diff)
Dropdowns accessibility (#7318)
* Mark currently selected privacy setting in privacy dropdown

* Prevent Enter keypresses from triggering dropdown display toggle twice

* Give focus to first/selected item of dropdown menus

* Implement keyboard navigation in privacy dropdown

* Implement keyboard navigation in generic dropdown menus
Diffstat (limited to 'app/javascript/mastodon/features/compose/components/privacy_dropdown.js')
-rw-r--r--app/javascript/mastodon/features/compose/components/privacy_dropdown.js70
1 files changed, 57 insertions, 13 deletions
diff --git a/app/javascript/mastodon/features/compose/components/privacy_dropdown.js b/app/javascript/mastodon/features/compose/components/privacy_dropdown.js
index 6b22ba84a..a772c1c95 100644
--- a/app/javascript/mastodon/features/compose/components/privacy_dropdown.js
+++ b/app/javascript/mastodon/features/compose/components/privacy_dropdown.js
@@ -42,22 +42,65 @@ class PrivacyDropdownMenu extends React.PureComponent {
     }
   }
 
-  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();
+  handleKeyDown = e => {
+    const { items } = this.props;
+    const value = e.currentTarget.getAttribute('data-index');
+    const index = items.findIndex(item => {
+      return (item.value === value);
+    });
+    let element;
 
+    switch(e.key) {
+    case 'Escape':
       this.props.onClose();
-      this.props.onChange(value);
+      break;
+    case 'Enter':
+      this.handleClick(e);
+      break;
+    case 'ArrowDown':
+      element = this.node.childNodes[index + 1];
+      if (element) {
+        element.focus();
+        this.props.onChange(element.getAttribute('data-index'));
+      }
+      break;
+    case 'ArrowUp':
+      element = this.node.childNodes[index - 1];
+      if (element) {
+        element.focus();
+        this.props.onChange(element.getAttribute('data-index'));
+      }
+      break;
+    case 'Home':
+      element = this.node.firstChild;
+      if (element) {
+        element.focus();
+        this.props.onChange(element.getAttribute('data-index'));
+      }
+      break;
+    case 'End':
+      element = this.node.lastChild;
+      if (element) {
+        element.focus();
+        this.props.onChange(element.getAttribute('data-index'));
+      }
+      break;
     }
   }
 
+  handleClick = e => {
+    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);
+    if (this.focusedItem) this.focusedItem.focus();
     this.setState({ mounted: true });
   }
 
@@ -70,6 +113,10 @@ class PrivacyDropdownMenu extends React.PureComponent {
     this.node = c;
   }
 
+  setFocusRef = c => {
+    this.focusedItem = c;
+  }
+
   render () {
     const { mounted } = this.state;
     const { style, items, value } = this.props;
@@ -80,9 +127,9 @@ class PrivacyDropdownMenu extends React.PureComponent {
           // 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' style={{ ...style, opacity: opacity, transform: mounted ? `scale(${scaleX}, ${scaleY})` : null }} ref={this.setRef}>
+          <div className='privacy-dropdown__dropdown' style={{ ...style, opacity: opacity, transform: mounted ? `scale(${scaleX}, ${scaleY})` : null }} role='listbox' 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 role='option' tabIndex='0' key={item.value} data-index={item.value} onKeyDown={this.handleKeyDown} onClick={this.handleClick} className={classNames('privacy-dropdown__option', { active: item.value === value })} aria-selected={item.value === value} ref={item.value === value ? this.setFocusRef : null}>
                 <div className='privacy-dropdown__option__icon'>
                   <i className={`fa fa-fw fa-${item.icon}`} />
                 </div>
@@ -147,9 +194,6 @@ export default class PrivacyDropdown extends React.PureComponent {
 
   handleKeyDown = e => {
     switch(e.key) {
-    case 'Enter':
-      this.handleToggle(e);
-      break;
     case 'Escape':
       this.handleClose();
       break;