diff --git a/Picker.js b/Picker.js index b7cc15b..77453b9 100644 --- a/Picker.js +++ b/Picker.js @@ -4,7 +4,7 @@ // ...Or maybe a button with a popup menu // License: LGPLv3.0+ // (c) Vitaliy Filippov 2019+ -// Version 2019-09-03 +// Version 2020-04-27 import React from 'react'; import ReactDOM from 'react-dom'; @@ -78,15 +78,25 @@ export default class Picker extends React.Component this.picker = e; } + getInputProps() + { + return { + onFocus: this.focus, + onBlur: this.blur, + focused: this.state.focused, + ref: this.setInput, + }; + } + + renderPicker() + { + return this.props.renderPicker(); + } + render() { return ( - {this.props.renderInput({ - onFocus: this.focus, - onBlur: this.blur, - focused: this.state.focused, - ref: this.setInput, - })} + {this.props.renderInput(this.getInputProps())} {this.state.focused ?
- {this.props.renderPicker()} + {this.renderPicker()}
: null}
); diff --git a/PickerMenu.js b/PickerMenu.js new file mode 100644 index 0000000..28013bc --- /dev/null +++ b/PickerMenu.js @@ -0,0 +1,136 @@ +// Menu-like Picker variant with keyboard control +// Version 2020-04-27 +// License: LGPLv3.0+ +// (c) Vitaliy Filippov 2020+ + +import React from 'react'; +import PropTypes from 'prop-types'; + +import autocomplete_css from './autocomplete.css'; +import Picker from './Picker.js'; + +export default class PickerMenu extends Picker +{ + static propTypes = { + ...Picker.propTypes, + // menu options + items: PropTypes.array, + // additional text/items to render before menu items + beforeItems: PropTypes.any, + // additional text/items to render after menu items + afterItems: PropTypes.any, + // menuitem callback + onSelectItem: PropTypes.func, + // menuitem name key - default empty (render the item itself) + labelKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + // change theme (CSS module) for this input + theme: PropTypes.object, + } + + onKeyDown = (ev) => + { + if ((ev.which == 40 || ev.which == 38) && this.props.items.length) + { + // up / down + this.setState({ + active: this.state.active == null ? 0 : ( + (this.state.active + (event.which === 40 ? 1 : this.props.items.length-1)) % this.props.items.length + ), + }); + if (!this.state.focused) + { + this.focus(); + } + } + else if ((ev.which == 10 || ev.which == 13) && this.state.active != null && + this.state.active < this.props.items.length) + { + // enter + this.onMouseDown(); + } + } + + onMouseDown = () => + { + const sel = this.props.items[this.state.active]; + const f = this.props.onSelectItem; + f && f(sel); + } + + onMouseOver = (ev) => + { + let e = ev.target; + while (e && e != ev.currentTarget && !e.id) + { + e = e.parentNode; + } + if (e && e.id) + { + this.setState({ active: e.id }); + } + } + + animatePicker = (e) => + { + if (e) + { + e.style.visibility = 'hidden'; + e.style.overflowY = 'hidden'; + const anim = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.msRequestAnimationFrame; + anim(() => + { + e.style.visibility = ''; + e.style.maxHeight = '1px'; + anim(() => + { + e.style.maxHeight = '100%'; + const end = () => { e.style.overflowY = 'auto'; e.removeEventListener('transitionend', end); }; + e.addEventListener('transitionend', end); + }); + }); + } + } + + renderPicker = () => + { + const theme = this.props.theme || autocomplete_css; + return (
+ {this.props.beforeItems} + {this.props.items.map((e, i) => (
+ {!this.props.labelKey ? e : e[this.props.labelKey]} +
))} + {this.props.afterItems} +
); + } + + getInputProps() + { + return { + ...super.getInputProps(), + onKeyDown: this.onKeyDown, + }; + } + + componentDidUpdate() + { + super.componentDidUpdate(); + if (this.input) + { + if (this.prevHeight && this.input.offsetHeight != this.prevHeight) + { + this.calculateDirection(); + } + this.prevHeight = this.input.offsetHeight; + } + } + + componentDidMount() + { + this.componentDidUpdate(); + } +} + +delete PickerMenu.propTypes.renderPicker; diff --git a/Selectbox.js b/Selectbox.js index 66bb119..8b34e6e 100644 --- a/Selectbox.js +++ b/Selectbox.js @@ -1,5 +1,5 @@ // Simple Dropdown/Autocomplete with single/multiple selection and easy customisation via CSS modules -// Version 2019-09-15 +// Version 2020-04-27 // License: LGPLv3.0+ // (c) Vitaliy Filippov 2019+ @@ -7,7 +7,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import autocomplete_css from './autocomplete.css'; -import Picker from './Picker.js'; +import PickerMenu from './PickerMenu.js'; export default class Selectbox extends React.PureComponent { @@ -54,7 +54,6 @@ export default class Selectbox extends React.PureComponent state = { shown: false, - active: null, query: null, inputWidth: 20, } @@ -74,29 +73,6 @@ export default class Selectbox extends React.PureComponent } } - onKeyDown = (ev) => - { - if ((ev.which == 40 || ev.which == 38) && this.filtered_items.length) - { - // up / down - this.setState({ - active: this.state.active == null ? 0 : ( - (this.state.active + (event.which === 40 ? 1 : this.filtered_items.length-1)) % this.filtered_items.length - ), - }); - if (!this.picker.state.focused) - { - this.picker.focus(); - } - } - else if ((ev.which == 10 || ev.which == 13) && this.state.active != null && - this.state.active < this.filtered_items.length) - { - // enter - this.onMouseDown(); - } - } - clear = () => { this.setState({ query: null }); @@ -116,11 +92,11 @@ export default class Selectbox extends React.PureComponent } } - onMouseDown = () => + onSelectItem = (item) => { this.setState({ query: null }); this.picker.blur(); - const sel = this.filtered_items[this.state.active][this.props.valueKey||'id']; + const sel = item[this.props.valueKey||'id']; let value = sel; if (this.props.multiple) { @@ -141,19 +117,6 @@ export default class Selectbox extends React.PureComponent f && f(value); } - onMouseOver = (ev) => - { - let e = ev.target; - while (e && e != ev.currentTarget && !e.id) - { - e = e.parentNode; - } - if (e && e.id) - { - this.setState({ active: e.id }); - } - } - focusInput = () => { if (!this.props.disabled) @@ -168,14 +131,14 @@ export default class Selectbox extends React.PureComponent { return; } - if (!this.props.multiple && this.state.active === null) + if (!this.props.multiple && this.picker.state.active === null) { const v = this.props.value, vk = this.props.valueKey||'id'; for (let i = 0; i < this.filtered_items.length; i++) { if (v == this.filtered_items[i][vk]) { - this.setState({ active: i }); + this.picker.setState({ active: i }); break; } } @@ -213,7 +176,7 @@ export default class Selectbox extends React.PureComponent onBlur={this.onBlur} value={value} onChange={this.setQuery} - onKeyDown={this.onKeyDown} + onKeyDown={p.onKeyDown} />; return (
); } - animateSuggestions = (e) => - { - if (e) - { - e.style.visibility = 'hidden'; - e.style.overflowY = 'hidden'; - const anim = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.msRequestAnimationFrame; - anim(() => - { - e.style.visibility = ''; - e.style.maxHeight = '1px'; - anim(() => - { - e.style.maxHeight = '100%'; - const end = () => { e.style.overflowY = 'auto'; e.removeEventListener('transitionend', end); }; - e.addEventListener('transitionend', end); - }); - }); - } - } - - renderSuggestions = () => - { - const theme = this.props.theme || autocomplete_css; - return (
- {this.filtered_items.map((e, i) => (
- {e[this.props.labelKey||'name']} -
))} - {this.props.suggestionMsg} -
); - } - setSizer = (e) => { this.sizer = e; @@ -334,12 +262,15 @@ export default class Selectbox extends React.PureComponent } this.prevProps = this.props; this.prevState = this.state; - return (); } @@ -348,11 +279,6 @@ export default class Selectbox extends React.PureComponent if (this.sizer) { this.setState({ inputWidth: this.sizer.offsetWidth }); - if (this.prevHeight && this.picker.input.offsetHeight != this.prevHeight) - { - this.picker.calculateDirection(); - } - this.prevHeight = this.picker.input.offsetHeight; } } diff --git a/button.css b/button.css new file mode 100644 index 0000000..d4565b6 --- /dev/null +++ b/button.css @@ -0,0 +1,90 @@ +/* extjs-like } + items={NAMES} + />
; } }