diff options
Diffstat (limited to 'devtools/client/shared/components/SearchBoxAutocompletePopup.js')
-rw-r--r-- | devtools/client/shared/components/SearchBoxAutocompletePopup.js | 150 |
1 files changed, 150 insertions, 0 deletions
diff --git a/devtools/client/shared/components/SearchBoxAutocompletePopup.js b/devtools/client/shared/components/SearchBoxAutocompletePopup.js new file mode 100644 index 0000000000..08aad18872 --- /dev/null +++ b/devtools/client/shared/components/SearchBoxAutocompletePopup.js @@ -0,0 +1,150 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { + Component, +} = require("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); + +class SearchBoxAutocompletePopup extends Component { + static get propTypes() { + return { + /** + * autocompleteProvider takes search-box's entire input text as `filter` argument + * ie. "is:cached pr" + * returned value is array of objects like below + * [{value: "is:cached protocol", displayValue: "protocol"}[, ...]] + * `value` is used to update the search-box input box for given item + * `displayValue` is used to render the autocomplete list + */ + autocompleteProvider: PropTypes.func.isRequired, + filter: PropTypes.string.isRequired, + onItemSelected: PropTypes.func.isRequired, + }; + } + + constructor(props, context) { + super(props, context); + this.state = this.computeState(props); + this.computeState = this.computeState.bind(this); + this.jumpToTop = this.jumpToTop.bind(this); + this.jumpToBottom = this.jumpToBottom.bind(this); + this.jumpBy = this.jumpBy.bind(this); + this.select = this.select.bind(this); + this.onMouseDown = this.onMouseDown.bind(this); + } + + // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507 + UNSAFE_componentWillReceiveProps(nextProps) { + if (this.props.filter === nextProps.filter) { + return; + } + this.setState(this.computeState(nextProps)); + } + + componentDidUpdate() { + if (this.refs.selected) { + this.refs.selected.scrollIntoView(false); + } + } + + computeState({ autocompleteProvider, filter }) { + const list = autocompleteProvider(filter); + const selectedIndex = list.length ? 0 : -1; + + return { list, selectedIndex }; + } + + /** + * Use this method to select the top-most item + * This method is public, called outside of the autocomplete-popup component. + */ + jumpToTop() { + this.setState({ selectedIndex: 0 }); + } + + /** + * Use this method to select the bottom-most item + * This method is public. + */ + jumpToBottom() { + this.setState({ selectedIndex: this.state.list.length - 1 }); + } + + /** + * Increment the selected index with the provided increment value. Will cycle to the + * beginning/end of the list if the index exceeds the list boundaries. + * This method is public. + * + * @param {number} increment - No. of hops in the direction + */ + jumpBy(increment = 1) { + const { list, selectedIndex } = this.state; + let nextIndex = selectedIndex + increment; + if (increment > 0) { + // Positive cycling + nextIndex = nextIndex > list.length - 1 ? 0 : nextIndex; + } else if (increment < 0) { + // Inverse cycling + nextIndex = nextIndex < 0 ? list.length - 1 : nextIndex; + } + this.setState({ selectedIndex: nextIndex }); + } + + /** + * Submit the currently selected item to the onItemSelected callback + * This method is public. + */ + select() { + if (this.refs.selected) { + this.props.onItemSelected(this.refs.selected.dataset.value); + } + } + + onMouseDown(e) { + e.preventDefault(); + this.setState( + { selectedIndex: Number(e.target.dataset.index) }, + this.select + ); + } + + render() { + const { list } = this.state; + + return ( + !!list.length && + dom.div( + { className: "devtools-autocomplete-popup devtools-monospace" }, + dom.ul( + { className: "devtools-autocomplete-listbox" }, + list.map((item, i) => { + const isSelected = this.state.selectedIndex == i; + const itemClassList = ["autocomplete-item"]; + + if (isSelected) { + itemClassList.push("autocomplete-selected"); + } + return dom.li( + { + key: i, + "data-index": i, + "data-value": item.value, + className: itemClassList.join(" "), + ref: isSelected ? "selected" : null, + onMouseDown: this.onMouseDown, + }, + item.displayValue + ); + }) + ) + ) + ); + } +} + +module.exports = SearchBoxAutocompletePopup; |