From b706b1d5e1ce7e38ab1f67ffbe7be8b74bdc5d7a Mon Sep 17 00:00:00 2001 From: Vladimir Varankin Date: Sun, 14 Aug 2016 21:29:41 +0300 Subject: [PATCH 1/5] Menu: Scroll to first selected item on mount --- src/Menu/index.js | 34 +++++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/src/Menu/index.js b/src/Menu/index.js index 15f0d27..eb2c36c 100644 --- a/src/Menu/index.js +++ b/src/Menu/index.js @@ -15,10 +15,13 @@ class Menu extends Component { constructor(props) { super(props); + const value = this._validateValue(this.props.value); + this.state = { ...this.state, - value: this._validateValue(this.props.value), + value, hoveredIndex: null, + focusedIndex: null, }; this._cachedChildren = null; @@ -40,6 +43,11 @@ class Menu extends Component { if (this.props.value !== this.state.value) { this.props.onChange(this.state.value, this.props); } + + const selectedIdx = this._getFirstSelectedChildIndex(); + if (selectedIdx) { + this.setState({ focusedIndex: selectedIdx }); + } } componentDidMount() { @@ -119,6 +127,20 @@ class Menu extends Component { return null; } + _getFirstSelectedChildIndex() { + const { value } = this.state; + const children = this._getChildren(); + + for (let i = 0; i < children.length; i++) { + const item = children[i]; + if (!item.props.disabled && value.indexOf(item.props.value) !== -1) { + return i; + } + } + + return this._getFirstEnabledChildIndex(); + } + _getFirstEnabledChildIndex() { return this._getChildren().indexOf(this._getFirstEnabledChild()); } @@ -204,7 +226,8 @@ class Menu extends Component { } return ( -
Date: Mon, 15 Aug 2016 03:31:21 +0300 Subject: [PATCH 2/5] Select: Refactoring --- src/Menu/index.js | 53 ++++++++++++++++++---------------- src/Select/index.js | 69 +++++++++++++++++---------------------------- 2 files changed, 54 insertions(+), 68 deletions(-) diff --git a/src/Menu/index.js b/src/Menu/index.js index eb2c36c..95c8aaa 100644 --- a/src/Menu/index.js +++ b/src/Menu/index.js @@ -318,7 +318,7 @@ class Menu extends Component { if (this.props.disabled) { className += ' menu_disabled'; } - if (this.state.focused) { + if (this.props.focused) { className += ' menu_focused'; } @@ -330,7 +330,7 @@ class Menu extends Component { } dispatchFocusChange(focused) { - this.props.onFocusChange(focused); + return this.props.onFocusChange(focused); } dispatchItemClick(e, itemProps) { @@ -341,6 +341,28 @@ class Menu extends Component { this.props.onItemClick(e, itemProps); } + selectNextItem(dir) { + const children = this._getChildren(); + const len = children.length; + if (!len) { + return; + } + + let nextIndex = this.state.hoveredIndex; + do { + nextIndex = (nextIndex + len + dir) % len; + if (nextIndex === this.state.hoveredIndex) { + return; + } + } while (children[nextIndex].props.disabled); + + if (nextIndex !== null) { + this._hoveredItemIndex = nextIndex; + this._shouldScrollToItem = true; + this.setState({ hoveredIndex: nextIndex }); + } + } + onItemHover(hovered, itemProps) { this.setState({ hoveredIndex: hovered ? itemProps.index : null, @@ -388,38 +410,19 @@ class Menu extends Component { focused: false, hoveredIndex: null, }); - - this.dispatchFocusChange(false); + this._hoveredItemIndex = null; + this.dispatchFocusChange(); } onKeyDown(e) { - if (this.props.disabled || !this.state.focused) { + if (this.props.disabled) { return; } if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { e.preventDefault(); - const children = this._getChildren(); - const len = children.length; - if (!len) { - return; - } - - const dir = (e.key === 'ArrowDown' ? 1 : -1); - let nextIndex = this.state.hoveredIndex; - do { - nextIndex = (nextIndex + len + dir) % len; - if (nextIndex === this.state.hoveredIndex) { - return; - } - } while (children[nextIndex].props.disabled); - - if (nextIndex !== null) { - this._hoveredItemIndex = nextIndex; - this._shouldScrollToItem = true; - this.setState({ hoveredIndex: nextIndex }); - } + this.selectNextItem(e.key === 'ArrowDown' ? 1 : -1); } else if (e.key === ' ' || e.key === 'Enter') { e.preventDefault(); diff --git a/src/Select/index.js b/src/Select/index.js index 901d341..3e0bb60 100644 --- a/src/Select/index.js +++ b/src/Select/index.js @@ -1,6 +1,5 @@ import React from 'react'; import ReactDOM from 'react-dom'; - import Component from '../Component'; import Button from '../Button'; import Popup from '../Popup'; @@ -20,14 +19,12 @@ class Select extends Component { }; this._wasPopupVisible = false; - this._shouldRestoreButtonFocus = false; - this._preventTrapMenuFocus = false; + this._shouldRestoreControlFocus = false; this._cachedChildren = null; + this.onKeyDown = this.onKeyDown.bind(this); this.onButtonClick = this.onButtonClick.bind(this); - this.onButtonKeyDown = this.onButtonKeyDown.bind(this); this.onMenuChange = this.onMenuChange.bind(this); - this.onMenuFocusChange = this.onMenuFocusChange.bind(this); this.onMenuItemClick = this.onMenuItemClick.bind(this); this.onMenuKeyDown = this.onMenuKeyDown.bind(this); this.onPopupRequestHide = this.onPopupRequestHide.bind(this); @@ -43,13 +40,11 @@ class Select extends Component { componentWillUpdate(_, nextState) { if (nextState.popupVisible !== this.state.popupVisible && !nextState.popupVisible) { this._wasPopupVisible = false; - this.setState({ menuFocused: false }); } } componentDidUpdate(prevProps) { - this._shouldRestoreButtonFocus = false; - this._preventTrapMenuFocus = false; + this._shouldRestoreControlFocus = false; // FIXME(narqo@): an ugly trick to prevent DOM-focus from jumping to the bottom of the page on first open // @see https://github.com/narqo/react-islands/issues/41 @@ -58,7 +53,6 @@ class Select extends Component { this.updateMenuWidth(); process.nextTick(() => { this.setState({ menuFocused: true }); - this.trapMenuFocus() }); } else if (this.props.value !== prevProps.value) { this.updateMenuWidth(); @@ -72,11 +66,11 @@ class Select extends Component { render() { return ( -
+
{this.renderInputs()} - {this.renderButton()} + {this.renderControl()} 0 ); return ( - + ); + } + + renderTextInput() { + const { theme, size, name, value } = this.props; + + return ( + + ); + } + + renderMenu() { + const { theme, size, disabled, mode, value } = this.props; + const { menuHeight } = this.state; + // NOTE: "nullify" the tabIndex of the Menu + const tabIndex = null; + + return ( + + {this.props.children} + + ); + } + + className() { + let className = 'select'; + + const theme = this.props.theme || this.context.theme; + if (theme) { + className += ' select_theme_' + theme; + } + if (this.props.size) { + className += ' select_size_' + this.props.size; + } + if (this.props.mode) { + className += ' select_mode_' + this.props.mode; + } + if (this.props.disabled) { + className += ' select_disabled'; + } + if (this.state.popupVisible) { + className += ' select_opened'; + } + if (this.state.textInputFocused || this.state.buttonFocused) { + className += ' select_focused'; + } + + if (this.props.className) { + className += ' ' + this.props.className; + } + return className; + } + + getDisplayedValue() { + const value = this.props.value; + let res = ''; + + this.getItems().some(item => { + if (value === item.props.value) { + res = Component.textValue(item); + return true; + } + return false; + }); + + return res; + } + + getItems() { + if (!this._cachedChildren) { + let items = []; + + React.Children.forEach(this.props.children, child => { + if (Component.is(child, Item)) { + items.push(child); + } else if (Component.is(child, Group)) { + // Предполагаем, что ничего, кроме Item внутри Group уже нет. + items = items.concat(React.Children.toArray(child.props.children)); + } + }); + + this._cachedChildren = items; + } + + return this._cachedChildren; + } + + getControl() { + return this.refs.control; + } + + getMenu() { + return this.refs.menu; + } + + updateMenuWidth() { + const controlWidth = ReactDOM.findDOMNode(this.getControl()).offsetWidth; + ReactDOM.findDOMNode(this.getMenu()).style['min-width'] = `${controlWidth}px`; + } + + onKeyDown(e) { + if (this.state.popupVisible) { + // NOTE(narqo@): allow to move focus to another focusable using + // @see https://www.w3.org/TR/wai-aria-practices-1.1/#menu + if (e.key === 'Tab') { + this._shouldRestoreControlFocus = true; + this.setState({ popupVisible: false }); + } else if (e.key === 'ArrowDown' || e.key === 'ArrowUp' || e.key === 'Enter') { + this.getMenu().onKeyDown(e); + } + } + } + + onButtonClick() { + this.setState({ popupVisible: !this.state.popupVisible }); + } + + onButtonKeyDown(e) { + if (!this.state.popupVisible && + ((e.key === 'ArrowDown' || e.key === 'ArrowUp') && !e.shiftKey)) { + this.setState({ popupVisible: true }); + } + } + + onButtonFocusChange(focused) { + this.setState({ buttonFocused: focused }) + } + + onMenuItemClick() { + if (this.props.mode === 'radio' || this.props.mode === 'radio-check') { + this._shouldRestoreControlFocus = true; + // NOTE(narqo@): select with mode radio* must be closed on click within + this.setState({ popupVisible: false }); + } + } + + onMenuChange([value]) { + this.props.onChange(value, this.props); + } + + onTextInputFocusChange(focused) { + this.setState({ textInputFocused: focused }); + focused && this.getMenu().onFocus(); + } + + onTextInputChange(value) { + this.props.onChange(value, this.props, { source: 'textinput' }); + + if (this.props.onInputChange) { + this.props.onInputChange(value, this.props); + } + } + + onPopupLayout({ layout }, popupProps) { + const { maxHeight } = this.props; + const { viewportOffset } = popupProps; + const { pageYOffset } = window; + + if (layout.direction.indexOf('top-') > -1) { + let newTop = maxHeight ? layout.bottom - maxHeight : layout.top; + layout.top = Math.max(newTop, pageYOffset + viewportOffset); + } else { + let newBottom = maxHeight ? layout.top + maxHeight : layout.bottom; + layout.bottom = Math.min(newBottom, pageYOffset + window.innerHeight - viewportOffset); + } + + const menuHeight = layout.bottom - layout.top; + + this.setState({ menuHeight }); + } + + onPopupRequestHide(_, reason) { + this._shouldRestoreControlFocus = reason === 'escapeKeyPress'; + this.setState({ popupVisible: false }); + } +} +*/ + +ComboBox.contextTypes = { + theme: React.PropTypes.string, +}; + +ComboBox.propTypes = { + theme: React.PropTypes.string, + size: React.PropTypes.string, + name: React.PropTypes.string, + value: React.PropTypes.any, + placeholder: React.PropTypes.string, + disabled: React.PropTypes.bool, + maxHeight: React.PropTypes.number, + onChange: React.PropTypes.func, + onInputChange: React.PropTypes.func, +}; + +ComboBox.defaultProps = { + placeholder: '—', + mode: 'radio', + maxHeight: 0, + onChange() {}, +}; + +export default ComboBox; From 2b45b89ae1f65c2a2d1a396000a1ced80ee0f6f4 Mon Sep 17 00:00:00 2001 From: Vladimir Varankin Date: Mon, 15 Aug 2016 13:57:15 +0300 Subject: [PATCH 4/5] Menu: Implement typeahead --- src/Menu/index.js | 85 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 82 insertions(+), 3 deletions(-) diff --git a/src/Menu/index.js b/src/Menu/index.js index 95c8aaa..4de281f 100644 --- a/src/Menu/index.js +++ b/src/Menu/index.js @@ -5,6 +5,9 @@ import Item from '../Item'; import Group from '../Group'; import MenuItem from './MenuItem'; +const TIMEOUT_KEYBOARD_SEARCH = 1500; +const KEY_CODE_SPACE = 32; + function appendItemToCache(item, cache) { if (Component.is(item, Item)) { cache.push(item); @@ -27,12 +30,19 @@ class Menu extends Component { this._cachedChildren = null; this._hoveredItemIndex = null; this._shouldScrollToItem = false; + this._lastTyping = { + char: '', + text: '', + index: 0, + time: 0, + }; this.onMouseUp = this.onMouseUp.bind(this); this.onMouseDown = this.onMouseDown.bind(this); this.onFocus = this.onFocus.bind(this); this.onBlur = this.onBlur.bind(this); this.onKeyDown = this.onKeyDown.bind(this); + this.onKeyPress = this.onKeyPress.bind(this); this.onItemClick = this.onItemClick.bind(this); this.onItemHover = this.onItemHover.bind(this); } @@ -231,6 +241,7 @@ class Menu extends Component { style={style} tabIndex={tabIndex} onKeyDown={this.onKeyDown} + onKeyPress={this.onKeyPress} onMouseDown={this.onMouseDown} onMouseUp={this.onMouseUp} onFocus={this.onFocus} @@ -341,6 +352,58 @@ class Menu extends Component { this.props.onItemClick(e, itemProps); } + searchIndexByKeyboardEvent(e) { + const timeNow = Date.now(); + const lastTyping = this._lastTyping; + + if (e.charCode <= KEY_CODE_SPACE || e.ctrlKey || e.altKey || e.metaKey) { + lastTyping.time = timeNow; + return null; + } + + const char = String.fromCharCode(e.charCode).toLowerCase(); + const isSameChar = char === lastTyping.char && lastTyping.text.length === 1; + const children = this._getChildren(); + + if (timeNow - lastTyping.time > TIMEOUT_KEYBOARD_SEARCH || isSameChar) { + lastTyping.text = char; + } else { + lastTyping.text += char; + } + + lastTyping.char = char; + lastTyping.time = timeNow; + + let nextIndex = lastTyping.index; + + // If key is pressed again, then continue to search to next menu item + if (isSameChar && Component.textValue(children[nextIndex]).search(lastTyping.char) === 0) { + nextIndex = nextIndex >= children.length - 1 ? 0 : nextIndex + 1; + } + + // 2 passes: from index to children.length and from 0 to index. + let i = nextIndex; + let len = children.length; + while (i < len) { + if (!children[i].props.disabled && + Component.textValue(children[i]) + .toLowerCase() + .search(lastTyping.text) === 0) { + lastTyping.index = i; + return i; + } + + i++; + + if (i === children.length) { + i = 0; + len = nextIndex; + } + } + + return null; + } + selectNextItem(dir) { const children = this._getChildren(); const len = children.length; @@ -357,12 +420,16 @@ class Menu extends Component { } while (children[nextIndex].props.disabled); if (nextIndex !== null) { - this._hoveredItemIndex = nextIndex; - this._shouldScrollToItem = true; - this.setState({ hoveredIndex: nextIndex }); + this.selectItemByIndex(nextIndex); } } + selectItemByIndex(index) { + this._hoveredItemIndex = index; + this._shouldScrollToItem = true; + this.setState({ hoveredIndex: index }); + } + onItemHover(hovered, itemProps) { this.setState({ hoveredIndex: hovered ? itemProps.index : null, @@ -436,6 +503,18 @@ class Menu extends Component { } } + onKeyPress(e) { + if (this.props.disabled || !this.state.focused) { + return; + } + + const hoveredIndex = this.searchIndexByKeyboardEvent(e); + + if (hoveredIndex !== null) { + this.selectItemByIndex(hoveredIndex); + } + } + onItemCheck(index) { const { mode } = this.props; if (!mode) { From 41da31d9be9bc30291b3aae28b706bc6e40896bc Mon Sep 17 00:00:00 2001 From: Vladimir Varankin Date: Mon, 15 Aug 2016 16:32:08 +0300 Subject: [PATCH 5/5] ComboBox: Implement component --- src/ComboBox/examples.js | 1 + src/ComboBox/index.js | 321 ++----------------------------------- src/Menu/MenuItem/index.js | 1 + src/Menu/index.js | 35 ++-- src/Select/examples.js | 2 +- 5 files changed, 45 insertions(+), 315 deletions(-) diff --git a/src/ComboBox/examples.js b/src/ComboBox/examples.js index 44b9237..845d8c7 100644 --- a/src/ComboBox/examples.js +++ b/src/ComboBox/examples.js @@ -17,6 +17,7 @@ class ComboBoxExample extends React.Component { this.setState({ value })} > diff --git a/src/ComboBox/index.js b/src/ComboBox/index.js index 3a4b297..e14f539 100644 --- a/src/ComboBox/index.js +++ b/src/ComboBox/index.js @@ -7,6 +7,11 @@ class ComboBox extends Select { constructor(props) { super(props); + this.state = { + ...this.state, + textInputValue: '', + }; + this.onKeyDown = this.onKeyDown.bind(this); this.onTextInputChange = this.onTextInputChange.bind(this); } @@ -15,6 +20,7 @@ class ComboBox extends Select { componentWillUpdate(_, nextState) { if (nextState.popupVisible !== this.state.popupVisible && !nextState.popupVisible) { this._wasPopupVisible = false; + this.setState({ textInputValue: '' }); this.getMenu().onBlur(); } } @@ -39,12 +45,15 @@ class ComboBox extends Select { renderTextInput() { const { theme, size } = this.props; - const value = this.props.value[0]; + const { textInputValue } = this.state; + const placeholder = this.props.value[0]; return ( @@ -89,312 +98,14 @@ class ComboBox extends Select { } onTextInputChange(value) { - this.props.onChange([value], this.props, { source: 'textinput' }); - - if (this.props.onInputChange) { - this.props.onInputChange(value, this.props); + if (this.props.onTextInputChange) { + this.props.onTextInputChange(value, this.props); } + this.setState({ textInputValue: value }); + this.getMenu().hoverItemByText(value); } } -/* -class ComboBox extends Component { - constructor(props) { - super(props); - - this.state = { - buttonFocused: false, - menuHeight: null, - textInputFocused: false, - popupVisible: false, - }; - - this._wasPopupVisible = false; - this._shouldRestoreControlFocus = false; - this._cachedChildren = null; - - this.onKeyDown = this.onKeyDown.bind(this); - this.onButtonClick = this.onButtonClick.bind(this); - this.onButtonKeyDown = this.onButtonKeyDown.bind(this); - this.onButtonFocusChange = this.onButtonFocusChange.bind(this); - this.onMenuChange = this.onMenuChange.bind(this); - this.onMenuItemClick = this.onMenuItemClick.bind(this); - this.onPopupRequestHide = this.onPopupRequestHide.bind(this); - this.onPopupLayout = this.onPopupLayout.bind(this); - this.onTextInputChange = this.onTextInputChange.bind(this); - this.onTextInputFocusChange = this.onTextInputFocusChange.bind(this); - } - - componentWillReceiveProps(nextProps) { - if (nextProps.children !== this.props.children) { - this._cachedChildren = null; - } - } - - componentWillUpdate(_, nextState) { - if (nextState.popupVisible !== this.state.popupVisible && !nextState.popupVisible) { - this._wasPopupVisible = false; - this.setState({ - textInputFocused: false, - }); - } - } - - componentDidUpdate(prevProps) { - this._shouldRestoreControlFocus = false; - - // FIXME(narqo@): an ugly trick to prevent DOM-focus from jumping to the bottom of the page on first open - // @see https://github.com/narqo/react-islands/issues/41 - if (!this._wasPopupVisible && this.state.popupVisible) { - this._wasPopupVisible = true; - this.updateMenuWidth(); - process.nextTick(() => { - this.setState({ textInputFocused: true }); - }); - } else if (this.props.value !== prevProps.value) { - this.updateMenuWidth(); - } - } - - componentWillUnmount() { - this.setState({ popupVisible: false }); - this._cachedChildren = null; - } - - render() { - return ( -
- {this.renderControl()} - - {this.renderMenu()} - -
- ); - } - - renderControl() { - return this.state.popupVisible ? this.renderTextInput() : this.renderButton() - } - - renderButton() { - const { theme, size, disabled, mode, value } = this.props; - const focused = (!disabled && this._shouldRestoreControlFocus) ? true : undefined; - const checked = ( - (mode === 'check' || mode === 'radio-check') && - Array.isArray(value) && value.length > 0 - ); - - return ( - - ); - } - - renderTextInput() { - const { theme, size, name, value } = this.props; - - return ( - - ); - } - - renderMenu() { - const { theme, size, disabled, mode, value } = this.props; - const { menuHeight } = this.state; - // NOTE: "nullify" the tabIndex of the Menu - const tabIndex = null; - - return ( - - {this.props.children} - - ); - } - - className() { - let className = 'select'; - - const theme = this.props.theme || this.context.theme; - if (theme) { - className += ' select_theme_' + theme; - } - if (this.props.size) { - className += ' select_size_' + this.props.size; - } - if (this.props.mode) { - className += ' select_mode_' + this.props.mode; - } - if (this.props.disabled) { - className += ' select_disabled'; - } - if (this.state.popupVisible) { - className += ' select_opened'; - } - if (this.state.textInputFocused || this.state.buttonFocused) { - className += ' select_focused'; - } - - if (this.props.className) { - className += ' ' + this.props.className; - } - return className; - } - - getDisplayedValue() { - const value = this.props.value; - let res = ''; - - this.getItems().some(item => { - if (value === item.props.value) { - res = Component.textValue(item); - return true; - } - return false; - }); - - return res; - } - - getItems() { - if (!this._cachedChildren) { - let items = []; - - React.Children.forEach(this.props.children, child => { - if (Component.is(child, Item)) { - items.push(child); - } else if (Component.is(child, Group)) { - // Предполагаем, что ничего, кроме Item внутри Group уже нет. - items = items.concat(React.Children.toArray(child.props.children)); - } - }); - - this._cachedChildren = items; - } - - return this._cachedChildren; - } - - getControl() { - return this.refs.control; - } - - getMenu() { - return this.refs.menu; - } - - updateMenuWidth() { - const controlWidth = ReactDOM.findDOMNode(this.getControl()).offsetWidth; - ReactDOM.findDOMNode(this.getMenu()).style['min-width'] = `${controlWidth}px`; - } - - onKeyDown(e) { - if (this.state.popupVisible) { - // NOTE(narqo@): allow to move focus to another focusable using - // @see https://www.w3.org/TR/wai-aria-practices-1.1/#menu - if (e.key === 'Tab') { - this._shouldRestoreControlFocus = true; - this.setState({ popupVisible: false }); - } else if (e.key === 'ArrowDown' || e.key === 'ArrowUp' || e.key === 'Enter') { - this.getMenu().onKeyDown(e); - } - } - } - - onButtonClick() { - this.setState({ popupVisible: !this.state.popupVisible }); - } - - onButtonKeyDown(e) { - if (!this.state.popupVisible && - ((e.key === 'ArrowDown' || e.key === 'ArrowUp') && !e.shiftKey)) { - this.setState({ popupVisible: true }); - } - } - - onButtonFocusChange(focused) { - this.setState({ buttonFocused: focused }) - } - - onMenuItemClick() { - if (this.props.mode === 'radio' || this.props.mode === 'radio-check') { - this._shouldRestoreControlFocus = true; - // NOTE(narqo@): select with mode radio* must be closed on click within - this.setState({ popupVisible: false }); - } - } - - onMenuChange([value]) { - this.props.onChange(value, this.props); - } - - onTextInputFocusChange(focused) { - this.setState({ textInputFocused: focused }); - focused && this.getMenu().onFocus(); - } - - onTextInputChange(value) { - this.props.onChange(value, this.props, { source: 'textinput' }); - - if (this.props.onInputChange) { - this.props.onInputChange(value, this.props); - } - } - - onPopupLayout({ layout }, popupProps) { - const { maxHeight } = this.props; - const { viewportOffset } = popupProps; - const { pageYOffset } = window; - - if (layout.direction.indexOf('top-') > -1) { - let newTop = maxHeight ? layout.bottom - maxHeight : layout.top; - layout.top = Math.max(newTop, pageYOffset + viewportOffset); - } else { - let newBottom = maxHeight ? layout.top + maxHeight : layout.bottom; - layout.bottom = Math.min(newBottom, pageYOffset + window.innerHeight - viewportOffset); - } - - const menuHeight = layout.bottom - layout.top; - - this.setState({ menuHeight }); - } - - onPopupRequestHide(_, reason) { - this._shouldRestoreControlFocus = reason === 'escapeKeyPress'; - this.setState({ popupVisible: false }); - } -} -*/ - ComboBox.contextTypes = { theme: React.PropTypes.string, }; @@ -408,7 +119,7 @@ ComboBox.propTypes = { disabled: React.PropTypes.bool, maxHeight: React.PropTypes.number, onChange: React.PropTypes.func, - onInputChange: React.PropTypes.func, + onTextInputChange: React.PropTypes.func, }; ComboBox.defaultProps = { diff --git a/src/Menu/MenuItem/index.js b/src/Menu/MenuItem/index.js index 0368627..fb3a007 100644 --- a/src/Menu/MenuItem/index.js +++ b/src/Menu/MenuItem/index.js @@ -90,6 +90,7 @@ MenuItem.propTypes = { type: React.PropTypes.string, disabled: React.PropTypes.bool, checked: React.PropTypes.bool, + hovered: React.PropTypes.bool, onClick: React.PropTypes.func, onHover: React.PropTypes.func, }; diff --git a/src/Menu/index.js b/src/Menu/index.js index 4de281f..50196e2 100644 --- a/src/Menu/index.js +++ b/src/Menu/index.js @@ -385,10 +385,7 @@ class Menu extends Component { let i = nextIndex; let len = children.length; while (i < len) { - if (!children[i].props.disabled && - Component.textValue(children[i]) - .toLowerCase() - .search(lastTyping.text) === 0) { + if (this._checkItemMatchText(children[i], lastTyping.text)) { lastTyping.index = i; return i; } @@ -404,7 +401,7 @@ class Menu extends Component { return null; } - selectNextItem(dir) { + hoverNextItem(dir) { const children = this._getChildren(); const len = children.length; if (!len) { @@ -420,16 +417,36 @@ class Menu extends Component { } while (children[nextIndex].props.disabled); if (nextIndex !== null) { - this.selectItemByIndex(nextIndex); + this.hoverItemByIndex(nextIndex); } } - selectItemByIndex(index) { + hoverItemByText(text) { + const children = this._getChildren(); + let i = 0; + while (i < children.length) { + if (this._checkItemMatchText(children[i], text)) { + this.hoverItemByIndex(i); + return; + } + i++; + } + this.hoverItemByIndex(0); + } + + hoverItemByIndex(index) { this._hoveredItemIndex = index; this._shouldScrollToItem = true; this.setState({ hoveredIndex: index }); } + _checkItemMatchText(item, text) { + return !item.props.disabled && + Component.textValue(item) + .toLowerCase() + .search(text) === 0; + } + onItemHover(hovered, itemProps) { this.setState({ hoveredIndex: hovered ? itemProps.index : null, @@ -489,7 +506,7 @@ class Menu extends Component { if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { e.preventDefault(); - this.selectNextItem(e.key === 'ArrowDown' ? 1 : -1); + this.hoverNextItem(e.key === 'ArrowDown' ? 1 : -1); } else if (e.key === ' ' || e.key === 'Enter') { e.preventDefault(); @@ -511,7 +528,7 @@ class Menu extends Component { const hoveredIndex = this.searchIndexByKeyboardEvent(e); if (hoveredIndex !== null) { - this.selectItemByIndex(hoveredIndex); + this.hoverItemByIndex(hoveredIndex); } } diff --git a/src/Select/examples.js b/src/Select/examples.js index d0f263c..8b47509 100644 --- a/src/Select/examples.js +++ b/src/Select/examples.js @@ -8,7 +8,7 @@ class SelectExample extends React.Component { constructor(props) { super(props); this.state = { - value: ['1'], + value: ['10'], clicks: 0, }; this.onSelectChange = this.onSelectChange.bind(this);