From 10cba651e67ee133aa430d0e814760a58648dfbc Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Fri, 13 Oct 2017 22:39:53 +0700 Subject: [PATCH] Better accessbility --- client/coral-ui/components/Dropdown.css | 18 ++++- client/coral-ui/components/Dropdown.js | 97 +++++++++++++++++++------ client/coral-ui/components/Icon.js | 7 +- client/coral-ui/components/Option.css | 11 +-- client/coral-ui/components/Option.js | 24 ++++-- 5 files changed, 118 insertions(+), 39 deletions(-) diff --git a/client/coral-ui/components/Dropdown.css b/client/coral-ui/components/Dropdown.css index eaa8b8b34..4f5126ab1 100644 --- a/client/coral-ui/components/Dropdown.css +++ b/client/coral-ui/components/Dropdown.css @@ -3,15 +3,27 @@ width: 150px; height: 36px; background: #2c2c2c; - padding: 10px 15px; box-sizing: border-box; color: white; border-radius: 2px; box-shadow: 0 2px 2px 0 rgba(0,0,0,.14), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.12); + + line-height: 1.4; + font-size: 13px; } -.dropdown { +.toggle { + padding: 10px 15px; cursor: pointer; + outline: none; + + &:focus { + background: #666; + } +} + +.toggleOpen { + background: #666; } .label { @@ -47,4 +59,4 @@ font-size: 1.2rem; vertical-align: middle; top: 13px; -} \ No newline at end of file +} diff --git a/client/coral-ui/components/Dropdown.js b/client/coral-ui/components/Dropdown.js index 40f23a8ef..1b83ef2f5 100644 --- a/client/coral-ui/components/Dropdown.js +++ b/client/coral-ui/components/Dropdown.js @@ -7,17 +7,25 @@ import ClickOutside from 'coral-framework/components/ClickOutside'; class Dropdown extends React.Component { - constructor() { - super(); + toggleRef = null; + firstOptionRef = null; + lastOptionRef = null; - this.state = { - isOpen: false - }; + state = { + isOpen: false + }; + + componentDidUpdate(_, prevState) { + if (!this.state.isOpen && prevState.isOpen) { + + // Refocus on the toggle element when menu closes. + this.toggleRef.focus(); + } } handleOptionKeyDown = (value, e) => { const code = e.which; - + // 13 = Return, 32 = Space if ((code === 13) || (code === 32)) { e.preventDefault(); @@ -28,7 +36,7 @@ class Dropdown extends React.Component { handleOptionClick = (value) => { this.setValue(value); } - + setValue = (value) => { if (this.props.onChange) { this.props.onChange(value); @@ -51,7 +59,7 @@ class Dropdown extends React.Component { handleKeyDown = (e) => { const code = e.which; - + // 13 = Return, 32 = Space if ((code === 13) || (code === 32)) { e.preventDefault(); @@ -65,10 +73,33 @@ class Dropdown extends React.Component { }); } + handleToggleRef = (ref) => this.toggleRef = ref; + + handleOptionsRef = (ref, index) => { + if (index === 0) { + this.firstOptionRef = ref; + } + if (index === this.props.children.length - 1) { + this.lastOptionRef = ref; + } + + // Focus on current value when menu opens. + if (ref) { + if (ref.props.value === this.props.value || index === 0 && !this.props.value) { + ref.focus(); + return; + } + } + } + + // Trap keyboard focus inside the dropdown until a value has been chosen. + trapFocusBegin = () => this.lastOptionRef.focus(); + trapFocusEnd = () => this.firstOptionRef.focus(); + renderLabel() { const options = React.Children.toArray(this.props.children); const option = options.find((option) => option.props.value === this.props.value); - + if (option) { return option.props.label ? option.props.label : option.props.value; } else if (this.props.value) { @@ -79,22 +110,41 @@ class Dropdown extends React.Component { } render() { - const {className = ''} = this.props; + const {className, toggleClassName} = this.props; return ( -
- {this.props.icon && } - {this.renderLabel()} - {this.state.isOpen ? : } - +
+
+ {this.props.icon &&
+ {this.state.isOpen && +
+
+
    + {React.Children.toArray(this.props.children) + .map((child, i) => + React.cloneElement(child, { + key: child.props.value, + ref: (ref) => this.handleOptionsRef(ref, i), + index: i, + onClick: () => this.handleOptionClick(child.props.value), + onKeyDown: (e) => this.handleOptionKeyDown(child.props.value, e) + }))} +
+
+
+ }
); @@ -103,6 +153,7 @@ class Dropdown extends React.Component { Dropdown.propTypes = { className: PropTypes.string, + toggleClassName: PropTypes.string, placeholder: PropTypes.string, icon: PropTypes.string, onChange: PropTypes.func.isRequired, diff --git a/client/coral-ui/components/Icon.js b/client/coral-ui/components/Icon.js index e0dfea3a7..344bd7e04 100644 --- a/client/coral-ui/components/Icon.js +++ b/client/coral-ui/components/Icon.js @@ -4,12 +4,13 @@ import {Icon as IconMDL} from 'react-mdl'; import cn from 'classnames'; import styles from './Icon.css'; -const Icon = ({className = '', name}) => ( - +const Icon = ({className = '', ...rest}) => ( + ); Icon.propTypes = { - name: PropTypes.string.isRequired + name: PropTypes.string.isRequired, + className: PropTypes.string, }; export default Icon; diff --git a/client/coral-ui/components/Option.css b/client/coral-ui/components/Option.css index 243a2716c..176047597 100644 --- a/client/coral-ui/components/Option.css +++ b/client/coral-ui/components/Option.css @@ -1,9 +1,10 @@ .option { padding: 10px; text-transform: capitalize; -} + outline: none; -.option:hover { - background-color: #eee; - cursor: pointer; -} \ No newline at end of file + &:focus, &:hover { + background-color: #d8d8d8; + cursor: pointer; + } +} diff --git a/client/coral-ui/components/Option.js b/client/coral-ui/components/Option.js index b7132e6b2..542d9c36f 100644 --- a/client/coral-ui/components/Option.js +++ b/client/coral-ui/components/Option.js @@ -3,11 +3,25 @@ import PropTypes from 'prop-types'; import styles from './Option.css'; import cn from 'classnames'; -const Option = ({className, label = '', onClick, onKeyDown}) => ( -
  • - {label} -
  • -); +class Option extends React.Component { + + ref = null; + + handleRef = (ref) => { + this.ref = ref; + }; + + focus = () => {this.ref.focus();} + + render() { + const {className, label = '', onClick, onKeyDown} = this.props; + return ( +
  • + {label} +
  • + ); + } +} Option.propTypes = { className: PropTypes.string,