Better accessbility

This commit is contained in:
Chi Vinh Le
2017-10-13 22:39:53 +07:00
parent 8f3aa789f5
commit 10cba651e6
5 changed files with 118 additions and 39 deletions
+15 -3
View File
@@ -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;
}
}
+74 -23
View File
@@ -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 (
<ClickOutside onClickOutside={this.hideMenu}>
<div className={cn(styles.dropdown, className)} onClick={this.handleClick} onKeyDown={this.handleKeyDown} role="button" aria-label="Dropdown" aria-haspopup="true" tabIndex="0">
{this.props.icon && <Icon name={this.props.icon} className={styles.icon} />}
<span className={styles.label}>{this.renderLabel()}</span>
{this.state.isOpen ? <Icon name="keyboard_arrow_up" className={styles.arrow} /> : <Icon name="keyboard_arrow_down" className={styles.arrow} />}
<ul className={cn(styles.list, {[styles.listActive] : this.state.isOpen})} role="menubar" aria-hidden="true">
{React.Children.toArray(this.props.children)
.map((child) =>
React.cloneElement(child, {
key: child.props.value,
onClick: () => this.handleOptionClick(child.props.value),
onKeyDown: (e) => this.handleOptionKeyDown(child.props.value, e)
}))}
</ul>
<div className={cn(styles.dropdown, className)}>
<div
className={cn(styles.toggle, toggleClassName, {[styles.toggleOpen]: this.state.isOpen})}
onClick={this.handleClick}
onKeyDown={this.handleKeyDown}
role="button"
aria-pressed={this.state.isOpen}
aria-haspopup="true"
tabIndex="0"
ref={this.handleToggleRef}
>
{this.props.icon && <Icon name={this.props.icon} className={styles.icon} aria-hidden="true" />}
<span className={styles.label}>{this.renderLabel()}</span>
{this.state.isOpen ? <Icon name="keyboard_arrow_up" className={styles.arrow} aria-hidden="true"/> : <Icon name="keyboard_arrow_down" className={styles.arrow} aria-hidden="true"/>}
</div>
{this.state.isOpen &&
<div>
<div tabIndex="0" onFocus={this.trapFocusBegin} />
<ul className={cn(styles.list, {[styles.listActive] : 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)
}))}
</ul>
<div tabIndex="0" onFocus={this.trapFocusEnd} />
</div>
}
</div>
</ClickOutside>
);
@@ -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,
+4 -3
View File
@@ -4,12 +4,13 @@ import {Icon as IconMDL} from 'react-mdl';
import cn from 'classnames';
import styles from './Icon.css';
const Icon = ({className = '', name}) => (
<IconMDL className={cn(styles.root, className)} name={name} />
const Icon = ({className = '', ...rest}) => (
<IconMDL className={cn(styles.root, className)} {...rest} />
);
Icon.propTypes = {
name: PropTypes.string.isRequired
name: PropTypes.string.isRequired,
className: PropTypes.string,
};
export default Icon;
+6 -5
View File
@@ -1,9 +1,10 @@
.option {
padding: 10px;
text-transform: capitalize;
}
outline: none;
.option:hover {
background-color: #eee;
cursor: pointer;
}
&:focus, &:hover {
background-color: #d8d8d8;
cursor: pointer;
}
}
+19 -5
View File
@@ -3,11 +3,25 @@ import PropTypes from 'prop-types';
import styles from './Option.css';
import cn from 'classnames';
const Option = ({className, label = '', onClick, onKeyDown}) => (
<li className={cn(styles.option, className)} onClick={onClick} onKeyDown={onKeyDown} role="menuitem" tabIndex="0">
{label}
</li>
);
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 (
<li className={cn(styles.option, className)} onClick={onClick} onKeyDown={onKeyDown} role="option" tabIndex="0" ref={this.handleRef}>
{label}
</li>
);
}
}
Option.propTypes = {
className: PropTypes.string,