SearchBox.js (6942B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 /* global window */ 6 7 "use strict"; 8 9 const { 10 createFactory, 11 createRef, 12 PureComponent, 13 } = require("resource://devtools/client/shared/vendor/react.mjs"); 14 const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.mjs"); 15 const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); 16 17 const { LocalizationHelper } = require("resource://devtools/shared/l10n.js"); 18 const l10n = new LocalizationHelper( 19 "devtools/client/locales/components.properties" 20 ); 21 22 loader.lazyGetter(this, "SearchBoxAutocompletePopup", function () { 23 return createFactory( 24 require("resource://devtools/client/shared/components/SearchBoxAutocompletePopup.js") 25 ); 26 }); 27 loader.lazyGetter(this, "MDNLink", function () { 28 return createFactory( 29 require("resource://devtools/client/shared/components/MdnLink.js") 30 ); 31 }); 32 33 loader.lazyRequireGetter( 34 this, 35 "KeyShortcuts", 36 "resource://devtools/client/shared/key-shortcuts.js" 37 ); 38 39 class SearchBox extends PureComponent { 40 static get propTypes() { 41 return { 42 autocompleteProvider: PropTypes.func, 43 delay: PropTypes.number, 44 keyShortcut: PropTypes.string, 45 learnMoreTitle: PropTypes.string, 46 learnMoreUrl: PropTypes.string, 47 onBlur: PropTypes.func, 48 onChange: PropTypes.func.isRequired, 49 onClearButtonClick: PropTypes.func, 50 onFocus: PropTypes.func, 51 // Optional function that will be called on the focus keyboard shortcut, before 52 // setting the focus to the input. If the function returns false, the input won't 53 // get focused. 54 onFocusKeyboardShortcut: PropTypes.func, 55 onKeyDown: PropTypes.func, 56 placeholder: PropTypes.string.isRequired, 57 summary: PropTypes.string, 58 summaryId: PropTypes.string, 59 summaryTooltip: PropTypes.string, 60 type: PropTypes.string, 61 initialValue: PropTypes.string, 62 }; 63 } 64 65 constructor(props) { 66 super(props); 67 68 this.state = { 69 value: props.initialValue || "", 70 focused: false, 71 }; 72 73 this.autocompleteRef = createRef(); 74 this.inputRef = createRef(); 75 76 this.onBlur = this.onBlur.bind(this); 77 this.onChange = this.onChange.bind(this); 78 this.onClearButtonClick = this.onClearButtonClick.bind(this); 79 this.onFocus = this.onFocus.bind(this); 80 this.onKeyDown = this.onKeyDown.bind(this); 81 } 82 83 componentDidMount() { 84 if (!this.props.keyShortcut) { 85 return; 86 } 87 88 this.shortcuts = new KeyShortcuts({ 89 window, 90 }); 91 this.shortcuts.on(this.props.keyShortcut, event => { 92 if (this.props.onFocusKeyboardShortcut?.(event)) { 93 return; 94 } 95 96 event.preventDefault(); 97 this.focus(); 98 }); 99 } 100 101 componentWillUnmount() { 102 if (this.shortcuts) { 103 this.shortcuts.destroy(); 104 } 105 106 // Clean up an existing timeout. 107 if (this.searchTimeout) { 108 clearTimeout(this.searchTimeout); 109 } 110 } 111 112 focus() { 113 if (this.inputRef) { 114 this.inputRef.current.focus(); 115 } 116 } 117 118 onChange(inputValue = "") { 119 if (this.state.value !== inputValue) { 120 this.setState({ 121 focused: true, 122 value: inputValue, 123 }); 124 } 125 126 if (!this.props.delay) { 127 this.props.onChange(inputValue); 128 return; 129 } 130 131 // Clean up an existing timeout before creating a new one. 132 if (this.searchTimeout) { 133 clearTimeout(this.searchTimeout); 134 } 135 136 // Execute the search after a timeout. It makes the UX 137 // smoother if the user is typing quickly. 138 this.searchTimeout = setTimeout(() => { 139 this.searchTimeout = null; 140 this.props.onChange(this.state.value); 141 }, this.props.delay); 142 } 143 144 onClearButtonClick() { 145 this.onChange(""); 146 147 if (this.props.onClearButtonClick) { 148 this.props.onClearButtonClick(); 149 } 150 } 151 152 onFocus() { 153 if (this.props.onFocus) { 154 this.props.onFocus(); 155 } 156 157 this.setState({ focused: true }); 158 } 159 160 onBlur() { 161 if (this.props.onBlur) { 162 this.props.onBlur(); 163 } 164 165 this.setState({ focused: false }); 166 } 167 168 onKeyDown(e) { 169 if (this.props.onKeyDown) { 170 this.props.onKeyDown(e); 171 } 172 173 const autocomplete = this.autocompleteRef.current; 174 if (!autocomplete || autocomplete.state.list.length <= 0) { 175 return; 176 } 177 178 switch (e.key) { 179 case "ArrowDown": 180 e.preventDefault(); 181 autocomplete.jumpBy(1); 182 break; 183 case "ArrowUp": 184 e.preventDefault(); 185 autocomplete.jumpBy(-1); 186 break; 187 case "PageDown": 188 e.preventDefault(); 189 autocomplete.jumpBy(5); 190 break; 191 case "PageUp": 192 e.preventDefault(); 193 autocomplete.jumpBy(-5); 194 break; 195 case "Enter": 196 case "Tab": 197 e.preventDefault(); 198 autocomplete.select(); 199 break; 200 case "Escape": 201 e.preventDefault(); 202 this.onBlur(); 203 break; 204 case "Home": 205 e.preventDefault(); 206 autocomplete.jumpToTop(); 207 break; 208 case "End": 209 e.preventDefault(); 210 autocomplete.jumpToBottom(); 211 break; 212 } 213 } 214 215 render() { 216 const { 217 autocompleteProvider, 218 summary, 219 summaryId, 220 summaryTooltip, 221 learnMoreTitle, 222 learnMoreUrl, 223 placeholder, 224 type = "search", 225 } = this.props; 226 const { value } = this.state; 227 const showAutocomplete = 228 autocompleteProvider && this.state.focused && value !== ""; 229 const showLearnMoreLink = learnMoreUrl && value === ""; 230 231 return dom.div( 232 { className: "devtools-searchbox" }, 233 dom.input({ 234 className: `devtools-${type}input`, 235 onBlur: this.onBlur, 236 onChange: e => this.onChange(e.target.value), 237 onFocus: this.onFocus, 238 onKeyDown: this.onKeyDown, 239 placeholder, 240 ref: this.inputRef, 241 value, 242 type: "search", 243 "aria-describedby": (summary && summaryId) || undefined, 244 }), 245 showLearnMoreLink && 246 MDNLink({ 247 title: learnMoreTitle, 248 url: learnMoreUrl, 249 }), 250 summary 251 ? dom.span( 252 { 253 className: "devtools-searchinput-summary", 254 id: summaryId, 255 title: summaryTooltip, 256 }, 257 summary 258 ) 259 : null, 260 dom.button({ 261 className: "devtools-searchinput-clear", 262 hidden: value === "", 263 onClick: this.onClearButtonClick, 264 title: l10n.getStr("searchBox.clearButtonTitle"), 265 }), 266 showAutocomplete && 267 SearchBoxAutocompletePopup({ 268 autocompleteProvider, 269 filter: value, 270 onItemSelected: itemValue => this.onChange(itemValue), 271 ref: this.autocompleteRef, 272 }) 273 ); 274 } 275 } 276 277 module.exports = SearchBox;