diff options
Diffstat (limited to 'devtools/client/shared/components/SearchBox.js')
-rw-r--r-- | devtools/client/shared/components/SearchBox.js | 269 |
1 files changed, 269 insertions, 0 deletions
diff --git a/devtools/client/shared/components/SearchBox.js b/devtools/client/shared/components/SearchBox.js new file mode 100644 index 0000000000..2733cfdc49 --- /dev/null +++ b/devtools/client/shared/components/SearchBox.js @@ -0,0 +1,269 @@ +/* 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/. */ + +/* global window */ + +"use strict"; + +const { + createFactory, + createRef, + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); + +loader.lazyGetter(this, "SearchBoxAutocompletePopup", function () { + return createFactory( + require("resource://devtools/client/shared/components/SearchBoxAutocompletePopup.js") + ); +}); +loader.lazyGetter(this, "MDNLink", function () { + return createFactory( + require("resource://devtools/client/shared/components/MdnLink.js") + ); +}); + +loader.lazyRequireGetter( + this, + "KeyShortcuts", + "resource://devtools/client/shared/key-shortcuts.js" +); + +class SearchBox extends PureComponent { + static get propTypes() { + return { + autocompleteProvider: PropTypes.func, + delay: PropTypes.number, + keyShortcut: PropTypes.string, + learnMoreTitle: PropTypes.string, + learnMoreUrl: PropTypes.string, + onBlur: PropTypes.func, + onChange: PropTypes.func.isRequired, + onClearButtonClick: PropTypes.func, + onFocus: PropTypes.func, + // Optional function that will be called on the focus keyboard shortcut, before + // setting the focus to the input. If the function returns false, the input won't + // get focused. + onFocusKeyboardShortcut: PropTypes.func, + onKeyDown: PropTypes.func, + placeholder: PropTypes.string.isRequired, + summary: PropTypes.string, + summaryTooltip: PropTypes.string, + type: PropTypes.string, + value: PropTypes.string, + }; + } + + constructor(props) { + super(props); + + this.state = { + value: props.value || "", + focused: false, + }; + + this.autocompleteRef = createRef(); + this.inputRef = createRef(); + + this.onBlur = this.onBlur.bind(this); + this.onChange = this.onChange.bind(this); + this.onClearButtonClick = this.onClearButtonClick.bind(this); + this.onFocus = this.onFocus.bind(this); + this.onKeyDown = this.onKeyDown.bind(this); + } + + componentDidMount() { + if (!this.props.keyShortcut) { + return; + } + + this.shortcuts = new KeyShortcuts({ + window, + }); + this.shortcuts.on(this.props.keyShortcut, event => { + if (this.props.onFocusKeyboardShortcut?.(event)) { + return; + } + + event.preventDefault(); + this.focus(); + }); + } + + componentWillUnmount() { + if (this.shortcuts) { + this.shortcuts.destroy(); + } + + // Clean up an existing timeout. + if (this.searchTimeout) { + clearTimeout(this.searchTimeout); + } + } + + focus() { + if (this.inputRef) { + this.inputRef.current.focus(); + } + } + + onChange(inputValue = "") { + if (this.state.value !== inputValue) { + this.setState({ + focused: true, + value: inputValue, + }); + } + + if (!this.props.delay) { + this.props.onChange(inputValue); + return; + } + + // Clean up an existing timeout before creating a new one. + if (this.searchTimeout) { + clearTimeout(this.searchTimeout); + } + + // Execute the search after a timeout. It makes the UX + // smoother if the user is typing quickly. + this.searchTimeout = setTimeout(() => { + this.searchTimeout = null; + this.props.onChange(this.state.value); + }, this.props.delay); + } + + onClearButtonClick() { + this.onChange(""); + + if (this.props.onClearButtonClick) { + this.props.onClearButtonClick(); + } + } + + onFocus() { + if (this.props.onFocus) { + this.props.onFocus(); + } + + this.setState({ focused: true }); + } + + onBlur() { + if (this.props.onBlur) { + this.props.onBlur(); + } + + this.setState({ focused: false }); + } + + onKeyDown(e) { + if (this.props.onKeyDown) { + this.props.onKeyDown(e); + } + + const autocomplete = this.autocompleteRef.current; + if (!autocomplete || autocomplete.state.list.length <= 0) { + return; + } + + switch (e.key) { + case "ArrowDown": + e.preventDefault(); + autocomplete.jumpBy(1); + break; + case "ArrowUp": + e.preventDefault(); + autocomplete.jumpBy(-1); + break; + case "PageDown": + e.preventDefault(); + autocomplete.jumpBy(5); + break; + case "PageUp": + e.preventDefault(); + autocomplete.jumpBy(-5); + break; + case "Enter": + case "Tab": + e.preventDefault(); + autocomplete.select(); + break; + case "Escape": + e.preventDefault(); + this.onBlur(); + break; + case "Home": + e.preventDefault(); + autocomplete.jumpToTop(); + break; + case "End": + e.preventDefault(); + autocomplete.jumpToBottom(); + break; + } + } + + render() { + const { + autocompleteProvider, + summary, + summaryTooltip, + learnMoreTitle, + learnMoreUrl, + placeholder, + type = "search", + } = this.props; + const { value } = this.state; + const showAutocomplete = + autocompleteProvider && this.state.focused && value !== ""; + const showLearnMoreLink = learnMoreUrl && value === ""; + + const inputClassList = [`devtools-${type}input`]; + + return dom.div( + { className: "devtools-searchbox" }, + dom.input({ + className: inputClassList.join(" "), + onBlur: this.onBlur, + onChange: e => this.onChange(e.target.value), + onFocus: this.onFocus, + onKeyDown: this.onKeyDown, + placeholder, + ref: this.inputRef, + value, + type: "search", + }), + showLearnMoreLink && + MDNLink({ + title: learnMoreTitle, + url: learnMoreUrl, + }), + summary + ? dom.span( + { + className: "devtools-searchinput-summary", + title: summaryTooltip || "", + }, + summary + ) + : null, + dom.button({ + className: "devtools-searchinput-clear", + hidden: value === "", + onClick: this.onClearButtonClick, + }), + showAutocomplete && + SearchBoxAutocompletePopup({ + autocompleteProvider, + filter: value, + onItemSelected: itemValue => this.onChange(itemValue), + ref: this.autocompleteRef, + }) + ); + } +} + +module.exports = SearchBox; |