Skip to content

Commit

Permalink
Keyboard Friendly Dropdown
Browse files Browse the repository at this point in the history
  • Loading branch information
chrismcv committed Jun 15, 2015
1 parent cb70f0d commit e96d313
Show file tree
Hide file tree
Showing 10 changed files with 495 additions and 75 deletions.
2 changes: 1 addition & 1 deletion docs/gulp/tasks/jshint.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ var shell = require('gulp-shell');
var handleErrors = require('../util/handleErrors');

gulp.task('jshint', shell.task([
'../node_modules/.bin/jsxhint --harmony "../src/**" "./src/app/**" --exclude ../src/utils/modernizr.custom.js'
'"../node_modules/.bin/jsxhint" --harmony "../src/**" "./src/app/**" --exclude ../src/utils/modernizr.custom.js'
])).on('error', handleErrors);
73 changes: 72 additions & 1 deletion docs/src/app/components/pages/components/text-fields.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ var React = require('react/addons');
var mui = require('mui');
var ClearFix = mui.ClearFix;
var TextField = mui.TextField;
var SelectField = mui.SelectField;
var StyleResizable = mui.Mixins.StyleResizable;
var ComponentDoc = require('../../component-doc.jsx');

Expand All @@ -18,6 +19,8 @@ var TextFieldsPage = React.createClass({
propValue: 'Prop Value',
floatingPropValue: 'Prop Value',
valueLinkValue: 'Value Link',
selectValue: undefined,
selectValueLinkValue: 4,
floatingValueLinkValue: 'Value Link'
};
},
Expand Down Expand Up @@ -76,6 +79,17 @@ var TextFieldsPage = React.createClass({
' hintText="Disabled Hint Text"\n' +
' disabled={true}\n' +
' defaultValue="Disabled With Value" />\n\n' +
'<SelectField\n'+
'value={this.state.selectValue}\n'+
'onChange={this._handleSelectValueChange}\n'+
'floatingLabelText="Select Field"\n'+
'menuItems={menuItems} />\n'+
'<SelectField\n'+
'valueLink={this.linkState("selectValueLinkValue")}\n'+
'floatingLabelText="Select Field"\n'+
'valueMember="id"\n'+
'displayMember="name"\n'+
'menuItems={arbitraryArrayMenuItems} />\n'+

'//Floating Hint Text Labels\n' +
'<TextField\n' +
Expand Down Expand Up @@ -117,7 +131,20 @@ var TextFieldsPage = React.createClass({
' hintText="Disabled Hint Text"\n' +
' disabled={true}\n' +
' defaultValue="Disabled With Value"\n' +
' floatingLabelText="Floating Label Text" />';
' floatingLabelText="Floating Label Text" />\n'+
'<TextField\n' +
' hintText="Custom Child input (e.g. password)"\n' +
' defaultValue="Custom Child input (e.g. password)"\n' +
' floatingLabelText="Custom Child input (e.g. password)">\n' +
' <input type="password" />\n' +
'</TextField>\n'+
'<TextField\n' +
' hintText="Disabled Child input (e.g. password)"\n' +
' disabled={true}\n' +
' defaultValue="Custom Child input (e.g. password)"\n' +
' floatingLabelText="Custom Child input (e.g. password)">\n' +
' <input type="password" />\n' +
'</TextField>';

var desc = 'This component extends the current input element and will support all of its props and events. It supports ' +
'valueLink and can be controlled or uncontrolled.' ;
Expand Down Expand Up @@ -231,6 +258,20 @@ var TextFieldsPage = React.createClass({
];

var styles = this.getStyles();
var menuItems = [
{ payload: '1', text: 'Never' },
{ payload: '2', text: 'Every Night' },
{ payload: '3', text: 'Weeknights' },
{ payload: '4', text: 'Weekends' },
{ payload: '5', text: 'Weekly' },
];
var arbitraryArrayMenuItems = [
{id:1, name:'Never'},
{id:2, name:'Every Night'},
{id:3, name:'Weeknights'},
{id:4, name:'Weekends'},
{id:5, name:'Weekly'}
];

return (
<ComponentDoc
Expand Down Expand Up @@ -281,6 +322,17 @@ var TextFieldsPage = React.createClass({
hintText="Disabled Hint Text"
disabled={true}
defaultValue="Disabled With Value" /><br/>
<SelectField
value={this.state.selectValue}
onChange={this._handleSelectValueChange}
floatingLabelText="Select Field"
menuItems={menuItems} />
<SelectField
valueLink={this.linkState('selectValueLinkValue')}
floatingLabelText="Select Field"
valueMember="id"
displayMember="name"
menuItems={arbitraryArrayMenuItems} />
</div>
<div style={styles.group}>
<TextField
Expand Down Expand Up @@ -323,6 +375,19 @@ var TextFieldsPage = React.createClass({
disabled={true}
defaultValue="Disabled With Value"
floatingLabelText="Floating Label Text" /><br/>
<TextField
hintText="Custom Child input (e.g. password)"
defaultValue="Custom Child input (e.g. password)"
floatingLabelText="Custom Child input (e.g. password)">
<input type="password" />
</TextField>
<TextField
hintText="Disabled Child input (e.g. password)"
disabled={true}
defaultValue="Custom Child input (e.g. password)"
floatingLabelText="Custom Child input (e.g. password)">
<input type="password" />
</TextField>
</div>
</ClearFix>
</ComponentDoc>
Expand Down Expand Up @@ -363,6 +428,12 @@ var TextFieldsPage = React.createClass({
});
},

_handleSelectValueChange: function(e) {
this.setState({
selectValue: e.target.value
});
},

_handleFloatingInputChange: function(e) {
this.setState({
floatingPropValue: e.target.value
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"prebuild": "rm -rf lib",
"jshint": "./node_modules/.bin/jsxhint --harmony 'src/**' --exclude src/utils/modernizr.custom.js",
"jshint": "'./node_modules/.bin/jsxhint' --harmony 'src/**' --exclude src/utils/modernizr.custom.js",
"build": "npm run jshint && ./node_modules/.bin/babel --stage 1 ./src --out-dir ./lib",
"prepublish": "npm run build"
},
Expand Down
132 changes: 114 additions & 18 deletions src/drop-down-menu.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ var React = require('react');
var StylePropable = require('./mixins/style-propable');
var Transitions = require('./styles/transitions');
var ClickAwayable = require('./mixins/click-awayable');
var KeyCode = require('./utils/key-code');
var DropDownArrow = require('./svg-icons/drop-down-arrow');
var Paper = require('./paper');
var Menu = require('./menu/menu');
Expand All @@ -19,29 +20,37 @@ var DropDownMenu = React.createClass({
// than just the parent.
propTypes: {
className: React.PropTypes.string,
displayMember: React.PropTypes.string,
valueMember: React.PropTypes.string,
autoWidth: React.PropTypes.bool,
onChange: React.PropTypes.func,
menuItems: React.PropTypes.array.isRequired,
menuItemStyle: React.PropTypes.object,
underlineStyle:React.PropTypes.object,
iconStyle:React.PropTypes.object,
labelStyle:React.PropTypes.object,
selectedIndex: React.PropTypes.number
},

getDefaultProps: function() {
return {
autoWidth: true
autoWidth: true,
valueMember:'payload',
displayMember:'text'
};
},

getInitialState: function() {
return {
open: false,
isHovered: false,
selectedIndex: this.props.selectedIndex || 0
selectedIndex: (this.props.hasOwnProperty('value') || this.props.hasOwnProperty('valueLink')) ? null :(this.props.selectedIndex || 0),
};
},

componentClickAway: function() {
this.setState({ open: false });
this.state.open = false;
this.setState(this.state);
},

componentDidMount: function() {
Expand All @@ -50,7 +59,9 @@ var DropDownMenu = React.createClass({
},

componentWillReceiveProps: function(nextProps) {
if (nextProps.hasOwnProperty('selectedIndex')) {
if (nextProps.hasOwnProperty('value') || nextProps.hasOwnProperty('valueLink')) {
return;
} else if (nextProps.hasOwnProperty('selectedIndex')) {
this._setSelectedIndex(nextProps);
}
},
Expand All @@ -72,7 +83,8 @@ var DropDownMenu = React.createClass({
position: 'relative',
display: 'inline-block',
height: this.getSpacing().desktopToolbarHeight,
fontSize: this.getSpacing().desktopDropDownMenuFontSize
fontSize: this.getSpacing().desktopDropDownMenuFontSize,
outline:'none'
},
control: {
cursor: 'pointer',
Expand All @@ -84,8 +96,7 @@ var DropDownMenu = React.createClass({
backgroundColor: backgroundColor,
height: '100%',
width: '100%',
opacity: (this.state.open) ? 0 :
(this.state.isHovered) ? 1 : 0
opacity:0
},
icon: {
position: 'absolute',
Expand Down Expand Up @@ -125,18 +136,52 @@ var DropDownMenu = React.createClass({
return styles;
},

getInputNode: function() {
var root = this.refs.root;
var item = this.props.menuItems[this.state.selectedIndex];
if (item)
root.value = item[this.props.displayMember];
return root;
},

render: function() {
var _this = this;
var styles = this.getStyles();

if (process.env.NODE_ENV !== 'production') {
console.assert(!!this.props.menuItems[this.state.selectedIndex], 'SelectedIndex of ' + this.state.selectedIndex + ' does not exist in menuItems.');
var selectedIndex = this.state.selectedIndex;
var displayValue = "";
if (selectedIndex) {
if (process.env.NODE_ENV !== 'production') {
console.assert(!!this.props.menuItems[selectedIndex], 'SelectedIndex of ' + selectedIndex + ' does not exist in menuItems.');
}
}
else {
if (this.props.valueMember && (this.props.valueLink || this.props.value))
{
var value = this.props.value || this.props.valueLink.value;
for (var i in this.props.menuItems)
if (this.props.menuItems[i][this.props.valueMember] === value)
selectedIndex = i;
}
}

var selectedItem = this.props.menuItems[selectedIndex];
if (selectedItem)
displayValue = selectedItem[this.props.displayMember];

var menuItems = this.props.menuItems.map(function(item){
item.text = item[_this.props.displayMember];
item.payload = item[_this.props.valueMember];
return item;
});

return (
<div
ref="root"
onMouseOut={this._handleMouseOut}
onMouseOver={this._handleMouseOver}
onKeyDown={this._onKeyDown}
onFocus={this.props.onFocus}
onBlur={this.props.onBlur}
className={this.props.className}
style={this.mergeAndPrefix(
styles.root,
Expand All @@ -145,21 +190,22 @@ var DropDownMenu = React.createClass({

<ClearFix style={this.mergeAndPrefix(styles.control)} onTouchTap={this._onControlClick}>
<Paper style={this.mergeAndPrefix(styles.controlBg)} zDepth={0} />
<div style={this.mergeAndPrefix(styles.label, this.state.open && styles.labelWhenOpen)}>
{this.props.menuItems[this.state.selectedIndex].text}
<div style={this.mergeAndPrefix(styles.label, this.state.open && styles.labelWhenOpen, this.props.labelStyle)}>
{displayValue}
</div>
<DropDownArrow style={this.mergeAndPrefix(styles.icon)}/>
<div style={this.mergeAndPrefix(styles.underline)}/>
<DropDownArrow style={this.mergeAndPrefix(styles.icon, this.props.iconStyle)}/>
<div style={this.mergeAndPrefix(styles.underline, this.props.underlineStyle)}/>
</ClearFix>

<Menu
ref="menuItems"
autoWidth={this.props.autoWidth}
selectedIndex={this.state.selectedIndex}
menuItems={this.props.menuItems}
selectedIndex={selectedIndex}
menuItems={menuItems}
menuItemStyle={this.mergeAndPrefix(styles.menuItem, this.props.menuItemStyle)}
hideable={true}
visible={this.state.open}
onRequestClose={this._onMenuRequestClose}
onItemTap={this._onMenuItemClick} />
</div>
);
Expand Down Expand Up @@ -187,20 +233,70 @@ var DropDownMenu = React.createClass({
this.setState({ open: !this.state.open });
},

_onKeyDown: function(e) {
switch(e.which) {
case KeyCode.UP:
if (!this.state.open)
this._selectPreviousItem();
else
if (e.altKey)
this.setState({open:false});
break;
case KeyCode.DOWN:
if (!this.state.open)
if (e.altKey)
this.setState({open:true});
else
this._selectNextItem();
break;
case KeyCode.ENTER:
case KeyCode.SPACE:
this.setState({open:true});
break;
default:
return; //important
}
e.preventDefault();
},

_onMenuItemClick: function(e, key, payload) {
if (this.props.onChange && this.state.selectedIndex !== key) this.props.onChange(e, key, payload);
if (this.props.onChange && this.state.selectedIndex !== key) {
var selectedItem = this.props.menuItems[this.state.selectedIndex];
if (selectedItem)
e.target.value = selectedItem[this.props.valueMember];

if (this.props.valueLink)
this.props.valueLink.requestChange(e.target.value)
else
this.props.onChange(e, key, payload);
}

this.setState({
selectedIndex: key,
open: false
value:e.target.value,
open: false,
isHovered:false
});
},

_onMenuRequestClose: function() {
this.setState({open:false});
},

_handleMouseOver: function() {
this.setState({isHovered: true});
},

_handleMouseOut: function() {
this.setState({isHovered: false});
},

_selectPreviousItem: function() {
this.setState({selectedIndex: Math.max(this.state.selectedIndex - 1, 0)});
},

_selectNextItem: function() {
this.setState({selectedIndex: Math.min(this.state.selectedIndex + 1, this.props.menuItems.length - 1)});
}

});
Expand Down
1 change: 1 addition & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ module.exports = {
RadioButton: require('./radio-button'),
RadioButtonGroup: require('./radio-button-group'),
RaisedButton: require('./raised-button'),
SelectField: require('./select-field'),
Slider: require('./slider'),
SvgIcon: require('./svg-icon'),
Icons: {
Expand Down
Loading

0 comments on commit e96d313

Please sign in to comment.