From 5e639e57133b7ea63362747678d40b1de6e80687 Mon Sep 17 00:00:00 2001 From: Andrew Durber Date: Sat, 20 Jan 2018 09:52:12 +0000 Subject: [PATCH] feat(Filter): Focus filter by default on open if defaultFocusFilter supplied fix #38 --- README.md | 9 ++++++--- dist/index.js | 2 +- docs/README.md | 9 ++++++--- src/Filter.js | 1 + src/Picky.js | 19 ++++++++++++++++++- tests/Picky.test.js | 15 +++++++++++++++ 6 files changed, 47 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 4de3ed9..038527a 100644 --- a/README.md +++ b/README.md @@ -81,9 +81,10 @@ import 'react-picky/dist/picky.css'; // Include CSS **Note** If you check the network tag in your dev tools, notice how all images aren't loaded, this is because it's a virtual list. ### With and without virtual list example -[![Edit [No virtual list] Simple example](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/9462xq75wy) -An example of a large multiselect with no virtual list, note the performance difference. +[![Edit [No virtual list] Simple example](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/9462xq75wy) + +An example of a large multiselect with no virtual list, note the performance difference. ## Props @@ -130,7 +131,8 @@ Picky.propTypes = { manySelectedPlaceholder: PropTypes.string, allSelectedPlaceholder: PropTypes.string, selectAllText: PropTypes.string, - renderSelectAll: PropTypes.func + renderSelectAll: PropTypes.func, + defaultFocusFilter: PropTypes.bool }; ``` @@ -161,6 +163,7 @@ Picky.propTypes = { * `allSelectedPlaceholder` - Default "%s selected" where %s is the number of items selected. This gets used when all options are selected. * `selectAllText` - Default "Select all", use this to override "Select all" text from top of dropdown when included with `includeSelectAll`. * `renderSelectAll` - Used for rendering a custom select all +* `defaultFocusFilter` - If set to true, will focus the filter by default when opened. ## Custom rendering diff --git a/dist/index.js b/dist/index.js index 76e72bc..69b3133 100644 --- a/dist/index.js +++ b/dist/index.js @@ -1 +1 @@ -"use strict";function _interopDefault(e){return e&&"object"==typeof e&&"default"in e?e.default:e}var React=require("react"),React__default=_interopDefault(React),PropTypes=_interopDefault(require("prop-types")),debounce=_interopDefault(require("lodash.debounce")),includes=_interopDefault(require("lodash.includes")),reactVirtualized=require("react-virtualized"),isEqual=_interopDefault(require("lodash.isequal")),format=_interopDefault(require("simple-format")),_typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},isDataObject=function(e,t,r){return"object"===(void 0===e?"undefined":_typeof(e))&&e.hasOwnProperty(t)&&e.hasOwnProperty(r)},generateGuid=function(){function e(){return Math.floor(65536*(1+Math.random())).toString(16).substring(1)}return e()+e()+"-"+e()+"-"+e()+"-"+e()+"-"+e()+e()+e()},hasItem=function(e,t,r,l,n){if(!e||!t)return!1;if(Array.isArray(e)){if(isDataObject(t,r,l)){var o=e.findIndex(function(e){return e[r]===t[r]});return n?o:o>-1}var a=e.indexOf(t);return n?a:a>-1}return isDataObject(t,r,l)?e[r]===t[r]:e===t},hasItemIndex=function(e,t,r,l){return hasItem(e,t,r,l,!0)},keyExtractor=function(e,t,r){return isDataObject(e,t,r)?e[t]:e},_createClass$1=function(){function e(e,t){for(var r=0;r-1?this.setState({selectedValue:[].concat(_toConsumableArray(r.slice(0,l)),_toConsumableArray(r.slice(l+1)))},function(){t.props.onChange(t.state.selectedValue)}):this.setState({selectedValue:[].concat(_toConsumableArray(this.state.selectedValue),[e])},function(){t.props.onChange(t.state.selectedValue)})}else this.setState({selectedValue:e},function(){t.props.onChange(t.state.selectedValue)})}},{key:"allSelected",value:function(){var e=this.state.selectedValue,t=this.props.options.slice(0),r=Array.isArray(e)?e.slice(0):[];return isEqual(t,r)}},{key:"toggleSelectAll",value:function(){var e=this;this.setState({selectedValue:this.state.allSelected?[]:this.props.options,allSelected:!this.state.allSelected},function(){e.props.onChange(e.state.selectedValue)})}},{key:"isControlled",value:function(){return null!=this.props.value}},{key:"isItemSelected",value:function(e){var t=this.isControlled()?this.props.value:this.state.selectedValue;return hasItem(t,e,this.props.labelKey,this.props.valueKey)}},{key:"renderVirtualList",value:function(e){var t=this;return React__default.createElement(reactVirtualized.AutoSizer,null,function(r){var l=r.width,n=r.height,o=l;return"test"===process.env.NODE_ENV&&(o=window.innerWidth),React__default.createElement(reactVirtualized.List,{defaultHeight:n,height:t.props.dropdownHeight||300,width:o,rowCount:e.length,rowHeight:t.cellMeasurerCache.rowHeight,rowRenderer:function(r){var l=r.index,n=r.key,o=r.parent,a=r.style,i=e[l],s=t.isItemSelected(i);return React__default.createElement(reactVirtualized.CellMeasurer,{cache:t.cellMeasurerCache,columnIndex:0,key:n,parent:o,rowIndex:l},t.props.render?t.props.render({index:l,style:a,item:i,isSelected:s,selectValue:t.selectValue,labelKey:t.props.labelKey,valueKey:t.props.valueKey,multiple:t.props.multiple}):React__default.createElement(Option,{key:n,style:a,item:i,isSelected:s,selectValue:t.selectValue,labelKey:t.props.labelKey,valueKey:t.props.valueKey,multiple:t.props.multiple,tabIndex:t.props.tabIndex,id:t.state.id+"-option-"+l}))}})})}},{key:"renderPlainList",value:function(e){var t=this;return e.map(function(e,r){var l=keyExtractor(e,t.props.valueKey,t.props.labelKey),n=t.isItemSelected(e);return"function"==typeof t.props.render?t.props.render({index:r,style:{},item:e,isSelected:n,selectValue:t.selectValue,labelKey:t.props.labelKey,valueKey:t.props.valueKey,multiple:t.props.multiple}):React__default.createElement(Option,{key:l,item:e,isSelected:n,selectValue:t.selectValue,labelKey:t.props.labelKey,valueKey:t.props.valueKey,multiple:t.props.multiple,tabIndex:t.props.tabIndex,id:t.state.id+"-option-"+r})})}},{key:"renderOptions",value:function(){var e=this.props,t=e.options,r=e.virtual,l=this.state.filtered?this.state.filteredOptions:t;return r?this.renderVirtualList(l):this.renderPlainList(l)}},{key:"onFilterChange",value:function(e){var t=this;if(!e.trim())return this.setState({filtered:!1,filteredOptions:[]});var r=this.props.options.filter(function(r){return isDataObject(r,t.props.labelKey,t.props.valueKey)?includes(String(r[t.props.labelKey]).toLowerCase(),e.toLowerCase()):includes(String(r).toLowerCase(),e.toLowerCase())});this.setState({filtered:!0,filteredOptions:r},function(){t.props.onFiltered&&t.props.onFiltered(r)})}},{key:"handleOutsideClick",value:function(e){var t=this.props.keepOpen||this.props.multiple;this.node&&this.node.contains(e.target)&&t||this.toggleDropDown()}},{key:"toggleDropDown",value:function(){var e=this;this.state.open?document.removeEventListener("click",this.handleOutsideClick,!1):document.addEventListener("click",this.handleOutsideClick,!1),this.setState({open:!this.state.open},function(){var t=e.state.open;t&&e.props.onOpen?e.props.onOpen():!t&&e.props.onClose&&e.props.onClose()})}},{key:"render",value:function(){var e=this,t=this.props,r=t.placeholder,l=t.value,n=t.multiple,o=t.numberDisplayed,a=t.includeSelectAll,i=t.includeFilter,s=t.filterDebounce,p=t.valueKey,c=t.labelKey,u=t.tabIndex,d=t.dropdownHeight,y=t.renderSelectAll,f=this.state.open,h="";f&&(h+=this.state.id+"-list");var b={};return this.props.virtual||(b={maxHeight:d,overflowY:"scroll"}),React__default.createElement("div",{ref:function(t){e.node=t},className:"picky",id:this.state.id,role:"combobox","aria-controls":this.state.id+"__button","aria-expanded":f,"aria-haspopup":f,"aria-owns":h,tabIndex:u},React__default.createElement("button",{id:this.state.id+"__button",type:"button",className:"picky__input",onClick:this.toggleDropDown},React__default.createElement(Placeholder,{allSelected:this.state.allSelected,placeholder:r,manySelectedPlaceholder:this.props.manySelectedPlaceholder,allSelectedPlaceholder:this.props.allSelectedPlaceholder,value:this.isControlled()?l:this.state.selectedValue,multiple:n,numberDisplayed:o,valueKey:p,labelKey:c})),f&&React__default.createElement("div",{className:"picky__dropdown",id:this.state.id+"-list",style:b},i&&React__default.createElement(Filter,{onFilterChange:s>0?debounce(this.onFilterChange,s):this.onFilterChange}),y&&y({filtered:this.state.filtered,allSelected:this.state.allSelected,toggleSelectAll:this.toggleSelectAll,tabIndex:u,multiple:n}),!y&&a&&n&&!this.state.filtered&&React__default.createElement("div",{tabIndex:u,role:"option",id:this.state.id+"-option-selectall","data-selectall":"true","aria-selected":this.state.allSelected,className:this.state.allSelected?"option selected":"option",onClick:this.toggleSelectAll,onKeyPress:this.toggleSelectAll},React__default.createElement("input",{type:"checkbox",readOnly:!0,onClick:this.toggleSelectAll,tabIndex:-1,checked:this.state.allSelected,"aria-label":"select all"}),React__default.createElement("span",null,this.props.selectAllText)),this.renderOptions()))}}]),t}();Picky$1.defaultProps={numberDisplayed:3,options:[],filterDebounce:150,dropdownHeight:300,onChange:function(){},itemHeight:35,tabIndex:0,keepOpen:!0,virtual:!0,selectAllText:"Select all"},Picky$1.propTypes={placeholder:PropTypes.string,value:PropTypes.oneOfType([PropTypes.array,PropTypes.string,PropTypes.number,PropTypes.object]),numberDisplayed:PropTypes.number,multiple:PropTypes.bool,options:PropTypes.array.isRequired,onChange:PropTypes.func.isRequired,open:PropTypes.bool,includeSelectAll:PropTypes.bool,includeFilter:PropTypes.bool,filterDebounce:PropTypes.number,dropdownHeight:PropTypes.number,onFiltered:PropTypes.func,onOpen:PropTypes.func,onClose:PropTypes.func,valueKey:PropTypes.string,labelKey:PropTypes.string,render:PropTypes.func,itemHeight:PropTypes.number,tabIndex:PropTypes.oneOfType([PropTypes.string,PropTypes.number]),keepOpen:PropTypes.bool,virtual:PropTypes.bool,manySelectedPlaceholder:PropTypes.string,allSelectedPlaceholder:PropTypes.string,selectAllText:PropTypes.string,renderSelectAll:PropTypes.func},Array.prototype.findIndex||Object.defineProperty(Array.prototype,"findIndex",{value:function(e){if(null==this)throw new TypeError('"this" is null or not defined');var t=Object(this),r=t.length>>>0;if("function"!=typeof e)throw new TypeError("predicate must be a function");for(var l=arguments[1],n=0;n-1}var a=e.indexOf(t);return n?a:a>-1}return isDataObject(t,r,l)?e[r]===t[r]:e===t},hasItemIndex=function(e,t,r,l){return hasItem(e,t,r,l,!0)},keyExtractor=function(e,t,r){return isDataObject(e,t,r)?e[t]:e},_createClass$1=function(){function e(e,t){for(var r=0;r-1?this.setState({selectedValue:[].concat(_toConsumableArray(r.slice(0,l)),_toConsumableArray(r.slice(l+1)))},function(){t.props.onChange(t.state.selectedValue)}):this.setState({selectedValue:[].concat(_toConsumableArray(this.state.selectedValue),[e])},function(){t.props.onChange(t.state.selectedValue)})}else this.setState({selectedValue:e},function(){t.props.onChange(t.state.selectedValue)})}},{key:"allSelected",value:function(){var e=this.state.selectedValue,t=this.props.options.slice(0),r=Array.isArray(e)?e.slice(0):[];return isEqual(t,r)}},{key:"toggleSelectAll",value:function(){var e=this;this.setState({selectedValue:this.state.allSelected?[]:this.props.options,allSelected:!this.state.allSelected},function(){e.props.onChange(e.state.selectedValue)})}},{key:"isControlled",value:function(){return null!=this.props.value}},{key:"isItemSelected",value:function(e){var t=this.isControlled()?this.props.value:this.state.selectedValue;return hasItem(t,e,this.props.labelKey,this.props.valueKey)}},{key:"renderVirtualList",value:function(e){var t=this;return React__default.createElement(reactVirtualized.AutoSizer,null,function(r){var l=r.width,n=r.height,o=l;return"test"===process.env.NODE_ENV&&(o=window.innerWidth),React__default.createElement(reactVirtualized.List,{defaultHeight:n,height:t.props.dropdownHeight||300,width:o,rowCount:e.length,rowHeight:t.cellMeasurerCache.rowHeight,rowRenderer:function(r){var l=r.index,n=r.key,o=r.parent,a=r.style,i=e[l],s=t.isItemSelected(i);return React__default.createElement(reactVirtualized.CellMeasurer,{cache:t.cellMeasurerCache,columnIndex:0,key:n,parent:o,rowIndex:l},t.props.render?t.props.render({index:l,style:a,item:i,isSelected:s,selectValue:t.selectValue,labelKey:t.props.labelKey,valueKey:t.props.valueKey,multiple:t.props.multiple}):React__default.createElement(Option,{key:n,style:a,item:i,isSelected:s,selectValue:t.selectValue,labelKey:t.props.labelKey,valueKey:t.props.valueKey,multiple:t.props.multiple,tabIndex:t.props.tabIndex,id:t.state.id+"-option-"+l}))}})})}},{key:"renderPlainList",value:function(e){var t=this;return e.map(function(e,r){var l=keyExtractor(e,t.props.valueKey,t.props.labelKey),n=t.isItemSelected(e);return"function"==typeof t.props.render?t.props.render({index:r,style:{},item:e,isSelected:n,selectValue:t.selectValue,labelKey:t.props.labelKey,valueKey:t.props.valueKey,multiple:t.props.multiple}):React__default.createElement(Option,{key:l,item:e,isSelected:n,selectValue:t.selectValue,labelKey:t.props.labelKey,valueKey:t.props.valueKey,multiple:t.props.multiple,tabIndex:t.props.tabIndex,id:t.state.id+"-option-"+r})})}},{key:"renderOptions",value:function(){var e=this.props,t=e.options,r=e.virtual,l=this.state.filtered?this.state.filteredOptions:t;return r?this.renderVirtualList(l):this.renderPlainList(l)}},{key:"onFilterChange",value:function(e){var t=this;if(!e.trim())return this.setState({filtered:!1,filteredOptions:[]});var r=this.props.options.filter(function(r){return isDataObject(r,t.props.labelKey,t.props.valueKey)?includes(String(r[t.props.labelKey]).toLowerCase(),e.toLowerCase()):includes(String(r).toLowerCase(),e.toLowerCase())});this.setState({filtered:!0,filteredOptions:r},function(){t.props.onFiltered&&t.props.onFiltered(r)})}},{key:"handleOutsideClick",value:function(e){var t=this.props.keepOpen||this.props.multiple;this.node&&this.node.contains(e.target)&&t||this.toggleDropDown()}},{key:"focusFilterInput",value:function(e){e&&this.props.defaultFocusFilter&&this.filter&&this.filter.filterInput&&this.filter.filterInput.focus()}},{key:"toggleDropDown",value:function(){var e=this;this.state.open?document.removeEventListener("click",this.handleOutsideClick,!1):document.addEventListener("click",this.handleOutsideClick,!1),this.setState({open:!this.state.open},function(){var t=e.state.open;e.focusFilterInput(t),t&&e.props.onOpen?e.props.onOpen():!t&&e.props.onClose&&e.props.onClose()})}},{key:"render",value:function(){var e=this,t=this.props,r=t.placeholder,l=t.value,n=t.multiple,o=t.numberDisplayed,a=t.includeSelectAll,i=t.includeFilter,s=t.filterDebounce,p=t.valueKey,u=t.labelKey,c=t.tabIndex,d=t.dropdownHeight,y=t.renderSelectAll,f=this.state.open,h="";f&&(h+=this.state.id+"-list");var b={};return this.props.virtual||(b={maxHeight:d,overflowY:"scroll"}),React__default.createElement("div",{ref:function(t){e.node=t},className:"picky",id:this.state.id,role:"combobox","aria-controls":this.state.id+"__button","aria-expanded":f,"aria-haspopup":f,"aria-owns":h,tabIndex:c},React__default.createElement("button",{id:this.state.id+"__button",type:"button",className:"picky__input",onClick:this.toggleDropDown},React__default.createElement(Placeholder,{allSelected:this.state.allSelected,placeholder:r,manySelectedPlaceholder:this.props.manySelectedPlaceholder,allSelectedPlaceholder:this.props.allSelectedPlaceholder,value:this.isControlled()?l:this.state.selectedValue,multiple:n,numberDisplayed:o,valueKey:p,labelKey:u})),f&&React__default.createElement("div",{className:"picky__dropdown",id:this.state.id+"-list",style:b},i&&React__default.createElement(Filter,{ref:function(t){return e.filter=t},onFilterChange:s>0?debounce(this.onFilterChange,s):this.onFilterChange}),y&&y({filtered:this.state.filtered,allSelected:this.state.allSelected,toggleSelectAll:this.toggleSelectAll,tabIndex:c,multiple:n}),!y&&a&&n&&!this.state.filtered&&React__default.createElement("div",{tabIndex:c,role:"option",id:this.state.id+"-option-selectall","data-selectall":"true","aria-selected":this.state.allSelected,className:this.state.allSelected?"option selected":"option",onClick:this.toggleSelectAll,onKeyPress:this.toggleSelectAll},React__default.createElement("input",{type:"checkbox",readOnly:!0,onClick:this.toggleSelectAll,tabIndex:-1,checked:this.state.allSelected,"aria-label":"select all"}),React__default.createElement("span",null,this.props.selectAllText)),this.renderOptions()))}}]),t}();Picky$1.defaultProps={numberDisplayed:3,options:[],filterDebounce:150,dropdownHeight:300,onChange:function(){},itemHeight:35,tabIndex:0,keepOpen:!0,virtual:!0,selectAllText:"Select all"},Picky$1.propTypes={placeholder:PropTypes.string,value:PropTypes.oneOfType([PropTypes.array,PropTypes.string,PropTypes.number,PropTypes.object]),numberDisplayed:PropTypes.number,multiple:PropTypes.bool,options:PropTypes.array.isRequired,onChange:PropTypes.func.isRequired,open:PropTypes.bool,includeSelectAll:PropTypes.bool,includeFilter:PropTypes.bool,filterDebounce:PropTypes.number,dropdownHeight:PropTypes.number,onFiltered:PropTypes.func,onOpen:PropTypes.func,onClose:PropTypes.func,valueKey:PropTypes.string,labelKey:PropTypes.string,render:PropTypes.func,itemHeight:PropTypes.number,tabIndex:PropTypes.oneOfType([PropTypes.string,PropTypes.number]),keepOpen:PropTypes.bool,virtual:PropTypes.bool,manySelectedPlaceholder:PropTypes.string,allSelectedPlaceholder:PropTypes.string,selectAllText:PropTypes.string,renderSelectAll:PropTypes.func,defaultFocusFilter:PropTypes.bool},Array.prototype.findIndex||Object.defineProperty(Array.prototype,"findIndex",{value:function(e){if(null==this)throw new TypeError('"this" is null or not defined');var t=Object(this),r=t.length>>>0;if("function"!=typeof e)throw new TypeError("predicate must be a function");for(var l=arguments[1],n=0;n (this.filterInput = input)} className="picky__filter__input" data-test="picky__filter__input" placeholder="Filter..." diff --git a/src/Picky.js b/src/Picky.js index 0f845c3..f736e02 100644 --- a/src/Picky.js +++ b/src/Picky.js @@ -44,14 +44,20 @@ class Picky extends React.PureComponent { this.allSelected = this.allSelected.bind(this); this.handleOutsideClick = this.handleOutsideClick.bind(this); this.isItemSelected = this.isItemSelected.bind(this); + this.focusFilterInput = this.focusFilterInput.bind(this); } componentWillMount() { const allSelected = this.allSelected(); + this.setState({ allSelected }); } + componentDidMount() { + this.focusFilterInput(this.state.open); + } + componentWillReceiveProps(nextProps) { if ( this.props.options !== nextProps.options || @@ -333,6 +339,14 @@ class Picky extends React.PureComponent { } this.toggleDropDown(); } + + focusFilterInput(isOpen) { + if (isOpen && this.props.defaultFocusFilter) { + if (this.filter && this.filter.filterInput) { + this.filter.filterInput.focus(); + } + } + } /** * Toggle state of dropdown * @@ -355,6 +369,7 @@ class Picky extends React.PureComponent { () => { const isOpen = this.state.open; // Prop callbacks + this.focusFilterInput(isOpen); if (isOpen && this.props.onOpen) { this.props.onOpen(); } else if (!isOpen && this.props.onClose) { @@ -430,6 +445,7 @@ class Picky extends React.PureComponent { > {includeFilter && ( (this.filter = filter)} onFilterChange={ filterDebounce > 0 ? debounce(this.onFilterChange, filterDebounce) @@ -525,7 +541,8 @@ Picky.propTypes = { manySelectedPlaceholder: PropTypes.string, allSelectedPlaceholder: PropTypes.string, selectAllText: PropTypes.string, - renderSelectAll: PropTypes.func + renderSelectAll: PropTypes.func, + defaultFocusFilter: PropTypes.bool }; export default Picky; diff --git a/tests/Picky.test.js b/tests/Picky.test.js index b0aaf66..c8c7aa3 100644 --- a/tests/Picky.test.js +++ b/tests/Picky.test.js @@ -431,6 +431,21 @@ describe('Picky', () => { expect(wrapper.find(Filter)).toHaveLength(1); }); + it('should be focused by default if defaultFocusFilter is true', () => { + //picky__filter__input + const wrapper = mount( + + ); + const input = wrapper.find(sel('picky__filter__input')); + + expect(input.instance()).toEqual(document.activeElement); + }); + it('should call onFilterChange prop when text has changed', () => { const onChange = jest.fn(); const wrapper = mount();