Repository: facebook/react
Pull request: Devtools Profiler: allow user to enter commit number



<!--
  Thanks for submitting a pull request!
  We appreciate you spending the time to work on these changes. Please provide enough information so that others can review your pull request. The three fields below are mandatory.

  Before submitting a pull request, please make sure the following is done:

  1. Fork [the repository](https://github.com/facebook/react) and create your branch from `master`.
  2. Run `yarn` in the repository root.
  3. If you've fixed a bug or added code that should be tested, add tests!
  4. Ensure the test suite passes (`yarn test`). Tip: `yarn test --watch TestName` is helpful in development.
  5. Run `yarn test-prod` to test in the production environment. It supports the same options as `yarn test`.
  6. If you need a debugger, run `yarn debug-test --watch TestName`, open `chrome://inspect`, and press "Inspect".
  7. Format your code with [prettier](https://github.com/prettier/prettier) (`yarn prettier`).
  8. Make sure your code lints (`yarn lint`). Tip: `yarn linc` to only check changed files.
  9. Run the [Flow](https://flowtype.org/) type checks (`yarn flow`).
  10. If you haven't already, complete the CLA.

  Learn more about contributing: https://reactjs.org/docs/how-to-contribute.html
-->

## Summary

When using the React Devtools Profiler, I often find myself with recordings that have many hundreds of commits:

<img width="496" alt="image" src="https://user-images.githubusercontent.com/433928/95030947-e9de7300-0667-11eb-9320-805e453b013e.png">

In some cases, those first commits are not useful (e.g., they may be renders that were triggered by `mousemove`), and I want to quickly jump to commits later in the recording. Currently, there is no easy way to jump to a specific commit. The navigation options are:

1. Use the arrow keys to inc/dec by 1
2. Use the left/right buttons to inc/dec by 1
3. Click and drag on the graph to inc/dec by larger amounts. 

This PR replaces the first number in the `x / y` fraction with an `<input>` element so the user can directly navigate to a specific commit.

<!-- Explain the **motivation** for making this change. What existing problem does the pull request solve? -->

## Test Plan

By default the UI looks mostly the same:

<img width="482" alt="image" src="https://user-images.githubusercontent.com/433928/95031088-d97ac800-0668-11eb-9d82-d2c4998d62d2.png">

I removed zero padding in favor of the input `size` property (which will ensure that the input element width is fixed regardless of the currently selected commit index).

Existing behavior still works:

https://v.usetapes.com/H3kDFaHBaf

(these Tapes links may not work in Chrome anymore - try a different browser if possible).

The input functions as a way to jump to a specific commit:

https://v.usetapes.com/ATWCfbwNYv

<img width="389" alt="image" src="https://user-images.githubusercontent.com/433928/95031544-6030a480-066b-11eb-9df0-35d8b6f5c28b.png">

Typing numbers below 1 or greater than the number of commits will default the value to the first or last commit, respectively.

<!-- Demonstrate the code is solid. Example: The exact commands you ran and their output, screenshots / videos if the pull request changes the user interface. -->


Changed files:
filename: packages/react-devtools-shared/src/devtools/views/Profiler/SnapshotSelector.css
patch: @@ -28,3 +28,19 @@
   justify-content: center;
   color: var(--color-dim);
 }
+
+.Input {
+  background: none;
+  font-size: var(--font-size-sans-normal);
+  text-align: right;
+  font-family: var(--font-family-monospace);
+  border: 1px solid transparent;
+  border-radius: 0.125rem;
+  padding: 0.125rem;
+  color: var(--color-attribute-editable-value);
+}
+
+.Input:focus {
+  background-color: var(--color-button-background-focus);
+  outline: none;
+}

filename: packages/react-devtools-shared/src/devtools/views/Profiler/SnapshotSelector.js
patch: @@ -8,7 +8,7 @@
  */
 
 import * as React from 'react';
-import {Fragment, useCallback, useContext, useMemo} from 'react';
+import {Fragment, useContext, useMemo} from 'react';
 import Button from '../Button';
 import ButtonIcon from '../ButtonIcon';
 import {ProfilerContext} from './ProfilerContext';
@@ -86,47 +86,89 @@ export default function SnapshotSelector(_: Props) {
 
   let label = null;
   if (numFilteredCommits > 0) {
-    label =
-      `${selectedFilteredCommitIndex + 1}`.padStart(
-        `${numFilteredCommits}`.length,
-        '0',
-      ) +
-      ' / ' +
-      numFilteredCommits;
+    const handleCommitInputChange = event => {
+      const value = parseInt(event.currentTarget.value, 10);
+      if (!isNaN(value)) {
+        const filteredIndex = Math.min(
+          Math.max(value - 1, 0),
+
+          // Snashots are shown to the user as 1-based
+          // but the indices within the profiler data array ar 0-based.
+          numFilteredCommits - 1,
+        );
+        selectCommitIndex(filteredCommitIndices[filteredIndex]);
+      }
+    };
+
+    const handleClick = event => {
+      event.currentTarget.select();
+    };
+
+    const handleKeyDown = event => {
+      switch (event.key) {
+        case 'ArrowDown':
+          viewPrevCommit();
+          event.stopPropagation();
+          break;
+        case 'ArrowUp':
+          viewNextCommit();
+          event.stopPropagation();
+          break;
+        default:
+          break;
+      }
+    };
+
+    const input = (
+      <input
+        className={styles.Input}
+        type="text"
+        inputMode="numeric"
+        pattern="[0-9]*"
+        value={selectedFilteredCommitIndex + 1}
+        size={`${numFilteredCommits}`.length}
+        onChange={handleCommitInputChange}
+        onClick={handleClick}
+        onKeyDown={handleKeyDown}
+      />
+    );
+
+    label = (
+      <Fragment>
+        {input} / {numFilteredCommits}
+      </Fragment>
+    );
   }
 
-  const viewNextCommit = useCallback(() => {
+  const viewNextCommit = () => {
     let nextCommitIndex = ((selectedFilteredCommitIndex: any): number) + 1;
     if (nextCommitIndex === filteredCommitIndices.length) {
       nextCommitIndex = 0;
     }
     selectCommitIndex(filteredCommitIndices[nextCommitIndex]);
-  }, [selectedFilteredCommitIndex, filteredCommitIndices, selectCommitIndex]);
-  const viewPrevCommit = useCallback(() => {
+  };
+  const viewPrevCommit = () => {
     let nextCommitIndex = ((selectedFilteredCommitIndex: any): number) - 1;
     if (nextCommitIndex < 0) {
       nextCommitIndex = filteredCommitIndices.length - 1;
     }
     selectCommitIndex(filteredCommitIndices[nextCommitIndex]);
-  }, [selectedFilteredCommitIndex, filteredCommitIndices, selectCommitIndex]);
+  };
 
-  const handleKeyDown = useCallback(
-    event => {
-      switch (event.key) {
-        case 'ArrowLeft':
-          viewPrevCommit();
-          event.stopPropagation();
-          break;
-        case 'ArrowRight':
-          viewNextCommit();
-          event.stopPropagation();
-          break;
-        default:
-          break;
-      }
-    },
-    [viewNextCommit, viewPrevCommit],
-  );
+  const handleKeyDown = event => {
+    switch (event.key) {
+      case 'ArrowLeft':
+        viewPrevCommit();
+        event.stopPropagation();
+        break;
+      case 'ArrowRight':
+        viewNextCommit();
+        event.stopPropagation();
+        break;
+      default:
+        break;
+    }
+  };
 
   if (commitData.length === 0) {
     return null;