diff --git a/HISTORY.md b/HISTORY.md index 594746b88..71cf36a40 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,18 @@ # History ---- + +## 6.3.0 / 2016-04-28 + +- support onBlur + +## 6.2.0 / 2016-04-20 + +- remove searchPlaceholder + +## 6.1.0 / 2016-04-18 + +- go with http://semantic-ui.com/modules/dropdown.html#search-selection + ## 6.0.0 / 2016-03-16 - remove defaultLabel/label diff --git a/README.md b/README.md index 1279cccd2..c6a70b15b 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,6 @@ React.render(c, container); |notFoundContent | specify content to show when no result matches. | String | 'Not Found' | |placeholder | select placeholder | React Node | | |showSearch | whether show search input in single mode | bool | true | -|searchPlaceholder | search input placeholder | React Node | | |allowClear | whether allowClear | bool | false | |tags | when tagging is enabled the user can select from pre-existing options or create a new tag by picking the first choice, which is what the user has typed into the search box so far. | bool | false | |maxTagTextLength | max tag text length to show | number | - | diff --git a/assets/index.less b/assets/index.less index 8e2a0cfee..21fc7a3a3 100644 --- a/assets/index.less +++ b/assets/index.less @@ -12,6 +12,13 @@ position: relative; vertical-align: middle; color: #666; + line-height: 28px; + + &-allow-clear { + .@{selectPrefixCls}-selection--single .@{selectPrefixCls}-selection__rendered { + padding-right: 40px; + } + } ul, li { margin: 0; @@ -31,6 +38,7 @@ top: 1px; right: 1px; width: 20px; + outline: none; b { border-color: #999999 transparent transparent transparent; border-style: solid; @@ -56,6 +64,12 @@ background-color: #fff; border-radius: 6px; border: 1px solid #d9d9d9; + + &__placeholder { + position: absolute; + top: 0; + color: #aaa; + } } &-focused &-selection { @@ -75,15 +89,24 @@ &-selection--single { height: 28px; + line-height: 28px; cursor: pointer; position: relative; + .@{selectPrefixCls}-selection-selected-value { + position: absolute; + left: 0; + top: 0; + } + .@{selectPrefixCls}-selection__rendered { + height: 28px; + position: relative; display: block; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; - padding-left: 10px; + margin-left: 10px; padding-right: 20px; line-height: 28px; } @@ -91,8 +114,9 @@ .@{selectPrefixCls}-selection__clear { font-weight: bold; position: absolute; - top: 5px; + top: 0; right: 20px; + line-height: 28px; &:after { content: '×' @@ -118,7 +142,6 @@ &-search__field__wrap { display: inline-block; - position: relative; } &-search__field__placeholder { @@ -129,7 +152,6 @@ } &-search--inline { - float: left; width: 100%; .@{selectPrefixCls}-search__field__wrap { width: 100%; @@ -141,6 +163,9 @@ background: transparent; outline: 0; width: 100%; + &::-ms-clear { + display: none; + } } > i { float: right; @@ -155,8 +180,12 @@ min-height: 28px; .@{selectPrefixCls}-search--inline { + float: left; width: auto; .@{selectPrefixCls}-search__field { + &__wrap { + width: auto; + } width: 0.75em; } } @@ -167,17 +196,16 @@ } .@{selectPrefixCls}-selection__rendered { - //display: inline-block; + position: relative; overflow: hidden; text-overflow: ellipsis; - padding-left: 8px; + margin-left: 8px; padding-bottom: 2px; - } - > ul > li { - margin-top: 4px; - height: 20px; - line-height: 20px; + .@{selectPrefixCls}-selection__choice { + margin-top: 4px; + line-height: 20px; + } } } @@ -284,9 +312,6 @@ z-index: 100; left: -9999px; top: -9999px; - //border-top: none; - //border-top-left-radius: 0; - //border-top-right-radius: 0; position: absolute; outline: none; @@ -441,28 +466,6 @@ } } - &-dropdown-search { - display: block; - padding: 4px; - .@{selectPrefixCls}-search__field__wrap { - width: 100%; - } - .@{selectPrefixCls}-search__field__placeholder { - top: 4px; - } - .@{selectPrefixCls}-search__field { - padding: 4px; - width: 100%; - box-sizing: border-box; - border: 1px solid #d9d9d9; - border-radius: 4px; - outline: none; - } - &.@{selectPrefixCls}-search--hide { - display: none; - } - } - &-open { .@{selectPrefixCls}-arrow b { border-color: transparent transparent #888 transparent; diff --git a/examples/force-suggest.js b/examples/force-suggest.js index 28c53807c..a76977909 100644 --- a/examples/force-suggest.js +++ b/examples/force-suggest.js @@ -55,7 +55,6 @@ const Search = React.createClass({ value={this.state.value} optionLabelProp="children" placeholder="placeholder" - searchPlaceholder="searchPlaceholder" style={{ width: 500 }} onChange={this.onChange} filterOption={false} diff --git a/examples/getPopupContainer.js b/examples/getPopupContainer.js index 12093e7b7..60f918641 100644 --- a/examples/getPopupContainer.js +++ b/examples/getPopupContainer.js @@ -43,7 +43,6 @@ const Test = React.createClass({
diff --git a/examples/single-animation.js b/examples/single-animation.js index 3d80070a5..f891a5564 100644 --- a/examples/single-animation.js +++ b/examples/single-animation.js @@ -18,7 +18,6 @@ const c1 = ( - {this.props.search} -
- {this.renderMenu()} -
+ return (
+ {this.renderMenu()}
); }, }); diff --git a/src/FilterMixin.js b/src/FilterMixin.js index 07f39ec8c..07387a4be 100644 --- a/src/FilterMixin.js +++ b/src/FilterMixin.js @@ -1,7 +1,9 @@ import React from 'react'; -import OptGroup from './OptGroup'; import { getValuePropValue, UNSELECTABLE_ATTRIBUTE, UNSELECTABLE_STYLE } from './util'; import { Item as MenuItem, ItemGroup as MenuItemGroup } from 'rc-menu'; +import warning from 'warning'; +import OptGroup from './OptGroup'; +import Option from './Option'; export default { filterOption(input, child) { @@ -44,6 +46,13 @@ export default { } return; } + + warning( + child.type === Option, + 'the children of `Select` should be `Select.Option` or `Select.OptGroup`, ' + + `instead of \`${child.type.name || child.type.displayName || child.type}\`.` + ); + const childValue = getValuePropValue(child); if (this.filterOption(inputValue, child)) { sel.push( { - this.setOpenState(false); - }, 150); - }, - onInputKeyDown(event) { const props = this.props; if (props.disabled) { @@ -287,20 +273,50 @@ const Select = React.createClass({ }); }, + onArrowClick(e) { + e.stopPropagation(); + this.setOpenState(!this.state.open, true); + }, + onPlaceholderClick() { - this.getInputDOMNode().focus(); + if (this.getInputDOMNode()) { + this.getInputDOMNode().focus(); + } }, onOuterFocus() { - this.setState({ - focused: true, - }); + this.clearBlurTime(); + this._focused = true; + this.updateFocusClassName(); + }, + + onPopupFocus() { + // fix ie scrollbar, focus element again + this.maybeFocus(true, true); }, onOuterBlur() { - this.setState({ - focused: false, - }); + this.blurTimer = setTimeout(() => { + this._focused = false; + this.updateFocusClassName(); + const props = this.props; + let { value } = this.state; + if (isSingleMode(props) && props.showSearch && + this.state.inputValue && props.defaultActiveFirstOption) { + const options = this._options || []; + if (options.length) { + const firstOption = findFirstMenuItem(options); + if (firstOption) { + value = [{ + key: firstOption.key, + label: this.getLabelFromOption(firstOption), + }]; + this.fireChange(value); + } + } + } + props.onBlur(this.getVLForOnChange(value)); + }, 10); }, onClearSelection(event) { @@ -311,11 +327,15 @@ const Select = React.createClass({ } event.stopPropagation(); if (state.inputValue || state.value.length) { - this.fireChange([]); - this.setOpenState(false); - this.setState({ - inputValue: '', - }); + if (this.state.value.length) { + this.fireChange([]); + } + this.setOpenState(false, true); + if (this.state.inputValue) { + this.setState({ + inputValue: '', + }); + } } }, @@ -372,16 +392,21 @@ const Select = React.createClass({ return this.dropdownContainer; }, - getSearchPlaceholderElement(hidden) { - const props = this.props; - let placeholder; - if (isMultipleOrTagsOrCombobox(props)) { - placeholder = props.placeholder || props.searchPlaceholder; - } else { - placeholder = props.searchPlaceholder; + getPlaceholderElement() { + const { props, state } = this; + let hidden = false; + if (state.inputValue) { + hidden = true; } + if (state.value.length) { + hidden = true; + } + if (isCombobox(props) && state.value.length === 1 && !state.value[0].key) { + hidden = false; + } + const placeholder = props.placeholder; if (placeholder) { - return (); +
); } return null; }, getInputElement() { const props = this.props; - return ( + return (
- {isMultipleOrTags(props) ? null : this.getSearchPlaceholderElement(!!this.state.inputValue)} - ); +
); }, getInputDOMNode() { @@ -427,27 +449,59 @@ const Select = React.createClass({ }, setOpenState(open, needFocus) { - this.clearDelayTimer(); - const { props, refs } = this; - // can not optimize, if children is empty - // if (this.state.open === open) { - // return; - // } - this.setState({ + const { props, state } = this; + if (state.open === open) { + this.maybeFocus(open, needFocus); + return; + } + const nextState = { open, - }, () => { - if (needFocus || open) { - if (open || isMultipleOrTagsOrCombobox(props)) { - const input = this.getInputDOMNode(); - if (input && document.activeElement !== input) { - input.focus(); - } - } else if (refs.selection) { - refs.selection.focus(); - } + }; + // clear search input value when open is false in singleMode. + if (!open && isSingleMode(props) && props.showSearch) { + nextState.inputValue = ''; + } + if (!open) { + this.maybeFocus(open, needFocus); + } + this.setState(nextState, () => { + if (open) { + this.maybeFocus(open, needFocus); } }); }, + clearBlurTime() { + if (this.blurTimer) { + clearTimeout(this.blurTimer); + this.blurTimer = null; + } + }, + updateFocusClassName() { + const { refs, props } = this; + // avoid setState and its side effect + if (this._focused) { + classes(refs.root).add(`${props.prefixCls}-focused`); + } else { + classes(refs.root).remove(`${props.prefixCls}-focused`); + } + }, + + maybeFocus(open, needFocus) { + if (needFocus || open) { + const input = this.getInputDOMNode(); + const { activeElement } = document; + if (input && (open || isMultipleOrTagsOrCombobox(this.props))) { + if (activeElement !== input) { + input.focus(); + } + } else { + const selection = this.refs.selection; + if (activeElement !== selection) { + selection.focus(); + } + } + } + }, addLabelToValue(props, value_) { let value = value_; @@ -466,13 +520,6 @@ const Select = React.createClass({ return value; }, - clearDelayTimer() { - if (this.delayTimer) { - clearTimeout(this.delayTimer); - this.delayTimer = null; - } - }, - removeSelected(selectedKey) { const props = this.props; if (props.disabled) { @@ -518,68 +565,102 @@ const Select = React.createClass({ }, renderTopControlNode() { - const { value } = this.state; + const { value, open, inputValue } = this.state; const props = this.props; - const { choiceTransitionName, prefixCls, maxTagTextLength } = props; - // single and not combobox, input is inside dropdown + const { choiceTransitionName, prefixCls, maxTagTextLength, showSearch } = props; + const className = `${prefixCls}-selection__rendered`; + // search input is inside topControlNode in single, multiple & combobox. 2016/04/13 + let innerNode = null; if (isSingleMode(props)) { - let innerNode = ( - {props.placeholder} - ); + let selectedValue = null; if (value.length) { - innerNode = {value[0].label}; - } - return ( - {innerNode} - ); - } - - let selectedValueNodes = []; - if (isMultipleOrTags(props)) { - selectedValueNodes = value.map((singleValue) => { - let content = singleValue.label; - const title = content; - if (maxTagTextLength && typeof content === 'string' && content.length > maxTagTextLength) { - content = `${content.slice(0, maxTagTextLength)}...`; + let showSelectedValue = false; + let opacity = 1; + if (!showSearch) { + showSelectedValue = true; + } else { + if (open) { + showSelectedValue = !inputValue; + if (showSelectedValue) { + opacity = 0.4; + } + } else { + showSelectedValue = true; + } } - return ( -
  • - {content} + {value[0].label} + ); + } + if (!showSearch) { + innerNode = [selectedValue]; + } else { + innerNode = [selectedValue,
    + {this.getInputElement()} +
    ]; + } + } else { + let selectedValueNodes = []; + if (isMultipleOrTags(props)) { + selectedValueNodes = value.map((singleValue) => { + let content = singleValue.label; + const title = content; + if (maxTagTextLength && + typeof content === 'string' && + content.length > maxTagTextLength) { + content = `${content.slice(0, maxTagTextLength)}...`; + } + return ( +
  • +
    {content}
    -
  • - ); - }); - } - selectedValueNodes.push(
  • - {this.getInputElement()} -
  • ); - const className = `${prefixCls}-selection__rendered`; - if (isMultipleOrTags(props) && choiceTransitionName) { - return ( + ); + }); + } + selectedValueNodes.push(
  • - {selectedValueNodes} - ); + {this.getInputElement()} +
  • ); + + if (isMultipleOrTags(props) && choiceTransitionName) { + innerNode = ( + {selectedValueNodes} + ); + } else { + innerNode = ; + } } - return (); + return (
    {this.getPlaceholderElement()}{innerNode}
    ); }, render() { @@ -594,6 +675,7 @@ const Select = React.createClass({ if (open) { options = this.renderFilterOptions(); } + this._options = options; if (open && (isMultipleOrTagsOrCombobox(props) || !props.showSearch) && !options.length) { open = false; } @@ -607,19 +689,30 @@ const Select = React.createClass({ [className]: !!className, [prefixCls]: 1, [`${prefixCls}-open`]: open, - [`${prefixCls}-focused`]: open || this.state.focused, + [`${prefixCls}-focused`]: open || !!this._focused, [`${prefixCls}-combobox`]: isCombobox(props), [`${prefixCls}-disabled`]: disabled, [`${prefixCls}-enabled`]: !disabled, + [`${prefixCls}-allow-clear`]: !!props.allowClear, }; - + const clearStyle = { + ...UNSELECTABLE_STYLE, + display: 'none', + }; + if (this.state.inputValue || this.state.value.length) { + clearStyle.display = 'block'; + } const clear = (); return ( - - - {ctrlNode} + {ctrlNode} {allowClear && !multiple ? clear : null} {multiple || !props.showArrow ? null : ( )} - {multiple ? - this.getSearchPlaceholderElement(!!this.state.inputValue || this.state.value.length) : - null} - - + + ); }, diff --git a/src/SelectTrigger.jsx b/src/SelectTrigger.jsx index 7746f64bc..32271867a 100644 --- a/src/SelectTrigger.jsx +++ b/src/SelectTrigger.jsx @@ -25,6 +25,7 @@ const BUILT_IN_PLACEMENTS = { const SelectTrigger = React.createClass({ propTypes: { + onPopupFocus: PropTypes.func, dropdownMatchSelectWidth: PropTypes.bool, dropdownAlign: PropTypes.object, visible: PropTypes.bool, @@ -87,25 +88,22 @@ const SelectTrigger = React.createClass({ this.popupMenu = menu; }, render() { - const props = this.props; + const { onPopupFocus, ...props } = this.props; const { multiple, visible, inputValue, dropdownAlign } = props; const dropdownPrefixCls = this.getDropdownPrefixCls(); const popupClassName = { [props.dropdownClassName]: !!props.dropdownClassName, [`${dropdownPrefixCls}--${multiple ? 'multiple' : 'single'}`]: 1, }; - const search = multiple || props.combobox || !props.showSearch ? null : ( - {props.inputElement} - ); const popupElement = this.getDropdownElement({ menuItems: props.options, - search, + onPopupFocus, multiple, inputValue, visible, }); return ( { done(); }); + it('should show selected value in singleMode when close', (done) => { + instance = ReactDOM.render( + , div); + expect($(ReactDOM.findDOMNode(instance)) + .find('.rc-select-selection-selected-value').length).to.be(1); + done(); + }); + + it('should show placeholder in singleMode when value is undefined', (done) => { + instance = ReactDOM.render( + , div); + expect($(ReactDOM.findDOMNode(instance)) + .find('.rc-select-selection__placeholder').length).to.be(1); + done(); + }); + describe('when open', function test() { this.timeout(400000); beforeEach((done) => { div = document.createElement('div'); + div.tabIndex = 0; document.body.appendChild(div); + instance = ReactDOM.render( , div); + + Simulate.click(ReactDOM.findDOMNode(instance.refs.selection)); done(); }); @@ -156,5 +182,10 @@ describe('Select', () => { done(); }, 100); }); + + it('should show search input in single selection trigger', (done) => { + expect($(instance.getInputDOMNode()).parents('.rc-select-open').length).to.be(1); + done(); + }); }); });