Files
firefox/devtools/client/shared/components/SearchBox.js
T
Nicolas Chevobbe e249389848 Bug 1844087 - [devtools] Make SearchBox input described by summary element. r=devtools-reviewers,accessibility-frontend-reviewers,ochameau,ayeddi.
This should allow the number of hidden messages to be notified to
screen reader users.
We update the Console FilterBar behavior to always show the "X hidden" message
when the input is focused, so screen reader can also pick up when no messages
are hidden.

Differential Revision: https://phabricator.services.mozilla.com/D213181
2024-06-17 06:31:26 +00:00

278 lines
6.8 KiB
JavaScript

/* 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,
summaryId: 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,
summaryId,
summaryTooltip,
learnMoreTitle,
learnMoreUrl,
placeholder,
type = "search",
} = this.props;
const { value } = this.state;
const showAutocomplete =
autocompleteProvider && this.state.focused && value !== "";
const showLearnMoreLink = learnMoreUrl && value === "";
return dom.div(
{ className: "devtools-searchbox" },
dom.input({
className: `devtools-${type}input`,
onBlur: this.onBlur,
onChange: e => this.onChange(e.target.value),
onFocus: this.onFocus,
onKeyDown: this.onKeyDown,
placeholder,
ref: this.inputRef,
value,
type: "search",
"aria-describedby": (summary && summaryId) || undefined,
}),
showLearnMoreLink &&
MDNLink({
title: learnMoreTitle,
url: learnMoreUrl,
}),
summary
? dom.span(
{
className: "devtools-searchinput-summary",
id: summaryId,
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;