summaryrefslogtreecommitdiffstats
path: root/devtools/client/shared/components/SearchBox.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/shared/components/SearchBox.js')
-rw-r--r--devtools/client/shared/components/SearchBox.js275
1 files changed, 275 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..a1c771bfb0
--- /dev/null
+++ b/devtools/client/shared/components/SearchBox.js
@@ -0,0 +1,275 @@
+/* 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");
+
+const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
+const l10n = new LocalizationHelper(
+ "devtools/client/locales/components.properties"
+);
+
+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,
+ title: l10n.getStr("searchBox.clearButtonTitle"),
+ }),
+ showAutocomplete &&
+ SearchBoxAutocompletePopup({
+ autocompleteProvider,
+ filter: value,
+ onItemSelected: itemValue => this.onChange(itemValue),
+ ref: this.autocompleteRef,
+ })
+ );
+ }
+}
+
+module.exports = SearchBox;