Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions locales/en-US/app.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -421,8 +421,11 @@ Home--chrome-extension-recording-instructions = Once installed, use the extensio
## IdleSearchField
## The component that is used for all the search inputs in the application.

IdleSearchField--search-input =
.placeholder = Enter filter terms
# `/` here overrides Firefox's Type Ahead Find shortcut, which would
# otherwise trigger an unhelpful find bar on top of the profiler UI.
# The shortcut itself is not localizable.
IdleSearchField--search-input2 =
.placeholder = Enter filter terms (/)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This requires a new string ID.
https://mozilla-l10n.github.io/documentation/localization/making_string_changes.html

In general, shortcuts are exposed as a separate string, so they can be localized where necessary. Does / work across locales? For example, that's only available with SHIFT on an Italian layout.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does / work across locales? For example, that's only available with SHIFT on an Italian layout.

It's meant to be the same key as the '/' that starts the type ahead feature of Firefox, so we filter markers instead of having an unusable find feature. The '/' is hardcoded at https://searchfox.org/firefox-main/rev/e374045fac6f19383a04d087d2930ced5c1551fe/toolkit/content/widgets/findbar.js#876 in Firefox, so no need to localize it on the profiler side.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it actually enabling the feature in the browser, or just a similar feature? What about other browsers?

We should still add a comment, e.g.

# `/` is a keyboard shortcut that mimics Firefox type ahead feature. The shortcut itself
# is not localizable.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it actually enabling the feature in the browser, or just a similar feature? What about other browsers?

It's not enabling the feature in the browser, but actually the opposite: the web page consuming the event prevents the browser feature from being triggered. I haven't tested other browsers, but gmail has a similar behavior: typing '/' focuses the filter box.


## JsTracerSettings
## JSTracer is an experimental feature and it's currently disabled. See Bug 1565788.
Expand Down
4 changes: 2 additions & 2 deletions src/components/shared/IdleSearchField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,13 +120,13 @@ export class IdleSearchField extends PureComponent<Props, State> {
onSubmit={this._onFormSubmit}
>
<Localized
id="IdleSearchField--search-input"
id="IdleSearchField--search-input2"
attrs={{ placeholder: true }}
>
<input
type="search"
name="search"
placeholder="Enter filter terms"
placeholder="Enter filter terms (/)"
className="idleSearchFieldInput photon-input"
required={true}
title={title ?? undefined}
Expand Down
1 change: 1 addition & 0 deletions src/components/shared/MarkerSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ class MarkerSettingsImpl extends PureComponent<Props, State> {
title="Only display markers that match a certain name"
currentSearchString={searchString}
onSearch={this._onSearch}
alsoFocusOnF={true}
/>
</Localized>
<Localized id="MarkerSettings--marker-filters" attrs={{ title: true }}>
Expand Down
1 change: 1 addition & 0 deletions src/components/shared/NetworkSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ class NetworkSettingsImpl extends PureComponent<Props> {
title="Only display network requests that match a certain name"
currentSearchString={searchString}
onSearch={this._onSearch}
alsoFocusOnF={true}
/>
</Localized>
</div>
Expand Down
60 changes: 59 additions & 1 deletion src/components/shared/PanelSearch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,18 @@ type Props = {
readonly title: string;
readonly currentSearchString: string;
readonly onSearch: (param: string) => void;
// When true, the "f" key (in addition to "/") will also focus the search
// field. This is opt-in because some panels (e.g. those showing frames) use
// "f" as a call node transform shortcut.
readonly alsoFocusOnF?: boolean;
};

type State = { searchFieldFocused: boolean };

export class PanelSearch extends React.PureComponent<Props, State> {
override state = { searchFieldFocused: false };
_searchFieldWrapper = React.createRef<HTMLDivElement>();

_onSearchFieldIdleAfterChange = (value: string) => {
this.props.onSearch(value);
};
Expand All @@ -32,6 +38,55 @@ export class PanelSearch extends React.PureComponent<Props, State> {
this.setState(() => ({ searchFieldFocused: false }));
};

override componentDidMount() {
window.addEventListener('keydown', this._handleGlobalKeyDown);
}

override componentWillUnmount() {
window.removeEventListener('keydown', this._handleGlobalKeyDown);
}

_handleGlobalKeyDown = (event: KeyboardEvent) => {
// Ignore key combinations involving modifier keys, so we don't interfere
// with browser or OS shortcuts.
if (event.ctrlKey || event.altKey || event.metaKey) {
return;
}

const isSlash = event.key === '/';
const isF = this.props.alsoFocusOnF && event.key === 'f';
if (!isSlash && !isF) {
return;
}

// Don't steal the key when the user is already typing in a text input,
// textarea, contenteditable element, or interacting with a select.
const target = event.target;
if (target instanceof HTMLElement) {
const tagName = target.tagName;
if (
tagName === 'INPUT' ||
tagName === 'TEXTAREA' ||
tagName === 'SELECT' ||
target.isContentEditable
) {
return;
}
}

const wrapper = this._searchFieldWrapper.current;
if (!wrapper) {
return;
}
const input = wrapper.querySelector<HTMLInputElement>(
'input[type="search"]'
);
if (input) {
event.preventDefault();
input.focus();
}
};

override render() {
const { label, title, currentSearchString, className } = this.props;
const { searchFieldFocused } = this.state;
Expand All @@ -40,7 +95,10 @@ export class PanelSearch extends React.PureComponent<Props, State> {
currentSearchString &&
!currentSearchString.includes(',');
return (
<div className={classNames('panelSearchField', className)}>
<div
className={classNames('panelSearchField', className)}
ref={this._searchFieldWrapper}
>
<label className="panelSearchFieldLabel">
{label + ' '}
<IdleSearchField
Expand Down
117 changes: 117 additions & 0 deletions src/test/components/PanelSearch.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/* 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/. */

import { fireEvent, screen } from '@testing-library/react';

import { render } from 'firefox-profiler/test/fixtures/testing-library';
import { PanelSearch } from 'firefox-profiler/components/shared/PanelSearch';
import { fireFullKeyPress } from 'firefox-profiler/test/fixtures/utils';
import { coerce } from 'firefox-profiler/utils/types';

describe('shared/PanelSearch', function () {
function setup({ alsoFocusOnF = false }: { alsoFocusOnF?: boolean } = {}) {
const onSearch = jest.fn();
const renderResult = render(
<PanelSearch
className="testPanelSearch"
label="Filter:"
title="Filter description"
currentSearchString=""
onSearch={onSearch}
alsoFocusOnF={alsoFocusOnF}
/>
);
return { ...renderResult, onSearch };
}

function getSearchInput(): HTMLInputElement {
return screen.getByRole('searchbox') as HTMLInputElement;
}

it('focuses the search input when the / key is pressed outside any input', () => {
setup();
const input = getSearchInput();
expect(input).not.toHaveFocus();

fireFullKeyPress(coerce<Window, HTMLElement>(window), { key: '/' });

expect(input).toHaveFocus();
});

it('does not steal the / key when focus is already inside an input', () => {
setup();
const input = getSearchInput();

// Add a separate input that will hold focus.
const otherInput = document.createElement('input');
otherInput.type = 'text';
document.body.appendChild(otherInput);
otherInput.focus();
expect(otherInput).toHaveFocus();

// Fire the keydown event from the focused input. The listener should
// bail out and leave focus where it was.
fireEvent.keyDown(otherInput, { key: '/' });

expect(otherInput).toHaveFocus();
expect(input).not.toHaveFocus();

document.body.removeChild(otherInput);
});

it('does not react to / combined with a modifier key', () => {
setup();
const input = getSearchInput();
expect(input).not.toHaveFocus();

fireEvent.keyDown(window, { key: '/', ctrlKey: true });
expect(input).not.toHaveFocus();

fireEvent.keyDown(window, { key: '/', metaKey: true });
expect(input).not.toHaveFocus();

fireEvent.keyDown(window, { key: '/', altKey: true });
expect(input).not.toHaveFocus();
});

it('focuses the search input when the f key is pressed and alsoFocusOnF is true', () => {
setup({ alsoFocusOnF: true });
const input = getSearchInput();
expect(input).not.toHaveFocus();

fireFullKeyPress(coerce<Window, HTMLElement>(window), { key: 'f' });

expect(input).toHaveFocus();
});

it('does not focus the search input on the f key when alsoFocusOnF is not set', () => {
setup();
const input = getSearchInput();
expect(input).not.toHaveFocus();

fireFullKeyPress(coerce<Window, HTMLElement>(window), { key: 'f' });

expect(input).not.toHaveFocus();
});

it('still uses / for focus even when alsoFocusOnF is true', () => {
setup({ alsoFocusOnF: true });
const input = getSearchInput();
expect(input).not.toHaveFocus();

fireFullKeyPress(coerce<Window, HTMLElement>(window), { key: '/' });

expect(input).toHaveFocus();
});

it('removes its global key listener on unmount', () => {
const { unmount } = setup();
const input = getSearchInput();
unmount();

// After unmount, pressing / should be a no-op (no focus jump back).
fireFullKeyPress(coerce<Window, HTMLElement>(window), { key: '/' });
expect(input).not.toHaveFocus();
});
});
2 changes: 1 addition & 1 deletion src/test/components/__snapshots__/FlameGraph.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -500,7 +500,7 @@ exports[`FlameGraph matches the snapshot 1`] = `
<input
class="idleSearchFieldInput photon-input"
name="search"
placeholder="Enter filter terms"
placeholder="Enter filter terms (/)"
required=""
title="Only display stacks which contain a function whose name matches this substring"
type="search"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -516,7 +516,7 @@ exports[`MarkerChart renders the normal marker chart and matches the snapshot 1`
<input
class="idleSearchFieldInput photon-input"
name="search"
placeholder="Enter filter terms"
placeholder="Enter filter terms (/)"
required=""
title="Only display markers that match a certain name"
type="search"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ exports[`MarkerTable renders some basic markers and updates when needed 1`] = `
<input
class="idleSearchFieldInput photon-input"
name="search"
placeholder="Enter filter terms"
placeholder="Enter filter terms (/)"
required=""
title="Only display markers that match a certain name"
type="search"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ exports[`NetworkChart renders NetworkChart correctly 1`] = `
<input
class="idleSearchFieldInput photon-input"
name="search"
placeholder="Enter filter terms"
placeholder="Enter filter terms (/)"
required=""
title="Only display network requests that match a certain name"
type="search"
Expand Down
Loading
Loading