Fix keyboard handling including scroll into view, add aria roles
This commit is contained in:
parent
b7fe06706d
commit
f67eedf843
1 changed files with 68 additions and 27 deletions
|
@ -20,6 +20,7 @@ import PropTypes from 'prop-types';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import AccessibleButton from './AccessibleButton';
|
import AccessibleButton from './AccessibleButton';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
import {Key} from "../../../Keyboard";
|
||||||
|
|
||||||
class MenuOption extends React.Component {
|
class MenuOption extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -48,9 +49,13 @@ class MenuOption extends React.Component {
|
||||||
mx_Dropdown_option_highlight: this.props.highlighted,
|
mx_Dropdown_option_highlight: this.props.highlighted,
|
||||||
});
|
});
|
||||||
|
|
||||||
return <div className={optClasses}
|
return <div
|
||||||
|
className={optClasses}
|
||||||
onClick={this._onClick}
|
onClick={this._onClick}
|
||||||
onMouseEnter={this._onMouseEnter}
|
onMouseEnter={this._onMouseEnter}
|
||||||
|
role="option"
|
||||||
|
aria-selected={this.props.highlighted}
|
||||||
|
ref={this.props.inputRef}
|
||||||
>
|
>
|
||||||
{ this.props.children }
|
{ this.props.children }
|
||||||
</div>;
|
</div>;
|
||||||
|
@ -66,6 +71,7 @@ MenuOption.propTypes = {
|
||||||
dropdownKey: PropTypes.string,
|
dropdownKey: PropTypes.string,
|
||||||
onClick: PropTypes.func.isRequired,
|
onClick: PropTypes.func.isRequired,
|
||||||
onMouseEnter: PropTypes.func.isRequired,
|
onMouseEnter: PropTypes.func.isRequired,
|
||||||
|
inputRef: PropTypes.any,
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -177,32 +183,43 @@ export default class Dropdown extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
_onInputKeyPress(e) {
|
_onInputKeyPress(e) {
|
||||||
// This needs to be on the keypress event because otherwise
|
// This needs to be on the keypress event because otherwise it can't cancel the form submission
|
||||||
// it can't cancel the form submission
|
if (e.key === Key.ENTER) {
|
||||||
if (e.key == 'Enter') {
|
|
||||||
this.setState({
|
|
||||||
expanded: false,
|
|
||||||
});
|
|
||||||
this.props.onOptionChange(this.state.highlightedOption);
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onInputKeyUp(e) {
|
_onInputKeyUp(e) {
|
||||||
// These keys don't generate keypress events and so needs to
|
// These keys don't generate keypress events and so needs to be on keyup
|
||||||
// be on keyup
|
switch (e.key) {
|
||||||
if (e.key == 'Escape') {
|
case Key.ENTER:
|
||||||
this.setState({
|
this.props.onOptionChange(this.state.highlightedOption);
|
||||||
expanded: false,
|
// fallthrough
|
||||||
});
|
case Key.ESCAPE:
|
||||||
} else if (e.key == 'ArrowDown') {
|
this.setState({
|
||||||
this.setState({
|
expanded: false,
|
||||||
highlightedOption: this._nextOption(this.state.highlightedOption),
|
});
|
||||||
});
|
break;
|
||||||
} else if (e.key == 'ArrowUp') {
|
case Key.ARROW_DOWN:
|
||||||
this.setState({
|
this.setState({
|
||||||
highlightedOption: this._prevOption(this.state.highlightedOption),
|
highlightedOption: this._nextOption(this.state.highlightedOption),
|
||||||
});
|
});
|
||||||
|
break;
|
||||||
|
case Key.ARROW_UP:
|
||||||
|
this.setState({
|
||||||
|
highlightedOption: this._prevOption(this.state.highlightedOption),
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case Key.HOME:
|
||||||
|
this.setState({
|
||||||
|
highlightedOption: this._firstOption(),
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case Key.END:
|
||||||
|
this.setState({
|
||||||
|
highlightedOption: this._lastOption(),
|
||||||
|
});
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -250,13 +267,36 @@ export default class Dropdown extends React.Component {
|
||||||
return keys[(index - 1) % keys.length];
|
return keys[(index - 1) % keys.length];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_firstOption() {
|
||||||
|
const keys = Object.keys(this.childrenByKey);
|
||||||
|
return keys[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
_lastOption() {
|
||||||
|
const keys = Object.keys(this.childrenByKey);
|
||||||
|
return keys[keys.length - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
_scrollIntoView(node) {
|
||||||
|
if (node) {
|
||||||
|
node.scrollIntoView({
|
||||||
|
block: "nearest",
|
||||||
|
behavior: "auto",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_getMenuOptions() {
|
_getMenuOptions() {
|
||||||
const options = React.Children.map(this.props.children, (child) => {
|
const options = React.Children.map(this.props.children, (child) => {
|
||||||
|
const highlighted = this.state.highlightedOption === child.key;
|
||||||
return (
|
return (
|
||||||
<MenuOption key={child.key} dropdownKey={child.key}
|
<MenuOption
|
||||||
highlighted={this.state.highlightedOption == child.key}
|
key={child.key}
|
||||||
|
dropdownKey={child.key}
|
||||||
|
highlighted={highlighted}
|
||||||
onMouseEnter={this._setHighlightedOption}
|
onMouseEnter={this._setHighlightedOption}
|
||||||
onClick={this._onMenuOptionClick}
|
onClick={this._onMenuOptionClick}
|
||||||
|
inputRef={highlighted ? this._scrollIntoView : undefined}
|
||||||
>
|
>
|
||||||
{ child }
|
{ child }
|
||||||
</MenuOption>
|
</MenuOption>
|
||||||
|
@ -280,13 +320,14 @@ export default class Dropdown extends React.Component {
|
||||||
if (this.state.expanded) {
|
if (this.state.expanded) {
|
||||||
if (this.props.searchEnabled) {
|
if (this.props.searchEnabled) {
|
||||||
currentValue = <input type="text" className="mx_Dropdown_option"
|
currentValue = <input type="text" className="mx_Dropdown_option"
|
||||||
ref={this._collectInputTextBox} onKeyPress={this._onInputKeyPress}
|
ref={this._collectInputTextBox}
|
||||||
|
onKeyPress={this._onInputKeyPress}
|
||||||
onKeyUp={this._onInputKeyUp}
|
onKeyUp={this._onInputKeyUp}
|
||||||
onChange={this._onInputChange}
|
onChange={this._onInputChange}
|
||||||
value={this.state.searchQuery}
|
value={this.state.searchQuery}
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
menu = <div className="mx_Dropdown_menu" style={menuStyle}>
|
menu = <div className="mx_Dropdown_menu" style={menuStyle} role="listbox">
|
||||||
{ this._getMenuOptions() }
|
{ this._getMenuOptions() }
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
@ -313,7 +354,7 @@ export default class Dropdown extends React.Component {
|
||||||
return <div className={classnames(dropdownClasses)} ref={this._collectRoot}>
|
return <div className={classnames(dropdownClasses)} ref={this._collectRoot}>
|
||||||
<AccessibleButton className="mx_Dropdown_input mx_no_textinput" onClick={this._onInputClick}>
|
<AccessibleButton className="mx_Dropdown_input mx_no_textinput" onClick={this._onInputClick}>
|
||||||
{ currentValue }
|
{ currentValue }
|
||||||
<span className="mx_Dropdown_arrow"></span>
|
<span className="mx_Dropdown_arrow" />
|
||||||
{ menu }
|
{ menu }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</div>;
|
</div>;
|
||||||
|
|
Loading…
Reference in a new issue