// Menu-like Picker variant with keyboard control // Version 2021-10-07 // 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, // don't hide the menu on item click keepOnClick: PropTypes.bool, // menuitem name key - default empty (render the item itself) labelKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), // menuitem CSS class key - default none classKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), // menuitem "disabled" key - default none disabledKey: 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 const dir = (event.which === 40 ? 1 : -1); let prev = this.state.active, active; if (prev == null) active = dir == 1 ? 0 : this.props.items.length-1; else active = (prev + this.props.items.length + dir) % this.props.items.length; if (this.props.disabledKey != null) { while (this.props.items[active][this.props.disabledKey] && active != prev) { active = (active + this.props.items.length + dir) % this.props.items.length; } if (this.props.items[active][this.props.disabledKey]) { active = null; } } this.setState({ active }); if (!this.state.focused) { this.focus(); } ev.preventDefault(); } else if (ev.which == 10 || ev.which == 13) { // enter if (!this.state.focused) { this.focus(); } else if (this.state.active != null && this.state.active < this.props.items.length) { this.onMouseDown(); } ev.preventDefault(); } } onMouseDown = (ev) => { if (this.state.active == null) { return; } if (!this.props.keepOnClick) { if (this.input) { this.input.blur(); } this.blur(); } else if (ev) { ev.preventDefault(); } const sel = this.props.items[this.state.active]; const f = this.props.onSelectItem; if (f) { f(sel); } else if (sel.onClick) { sel.onClick(); } } onMouseOver = (ev) => { let e = ev.target; while (e && e != ev.currentTarget && !e.getAttribute('data-idx')) { e = e.parentNode; } if (e && e.getAttribute('data-idx') && (this.props.disabledKey == null || !this.props.items[e.getAttribute('data-idx')][this.props.disabledKey])) { this.setState({ active: e.getAttribute('data-idx') }); } } renderPicker = () => { const theme = this.props.theme || autocomplete_css; if (!this.props.beforeItems && !this.props.items.length && !this.props.afterItems) { return null; } 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(prevProps) { super.componentDidUpdate(prevProps); if (this.state.focused) { if (this.prevHeight && this.input && this.input.offsetHeight != this.prevHeight || this.props.items != prevProps.items) { this.calculateDirection(); } if (this.input) { this.prevHeight = this.input.offsetHeight; } } } } delete PickerMenu.propTypes.renderPicker;