main
js 124 lines 3.54 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @flow
8 */
9
10 import type {SuspenseTimelineStep} from 'react-devtools-shared/src/frontend/types';
11
12 import typeof {SyntheticEvent} from 'react-dom-bindings/src/events/SyntheticEvent';
13
14 import * as React from 'react';
15 import {useContext, useRef} from 'react';
16 import {ElementTypeRoot} from 'react-devtools-shared/src/frontend/types';
17
18 import styles from './SuspenseScrubber.css';
19
20 import {getClassNameForEnvironment} from './SuspenseEnvironmentColors.js';
21
22 import Tooltip from '../Components/reach-ui/tooltip';
23 import {StoreContext} from '../context';
24
25 export default function SuspenseScrubber({
26 min,
27 max,
28 timeline,
29 value,
30 highlight,
31 onBlur,
32 onChange,
33 onFocus,
34 onHoverSegment,
35 onHoverLeave,
36 }: {
37 min: number,
38 max: number,
39 timeline: $ReadOnlyArray<SuspenseTimelineStep>,
40 value: number,
41 highlight: number,
42 onBlur?: () => void,
43 onChange: (index: number) => void,
44 onFocus?: () => void,
45 onHoverSegment: (index: number) => void,
46 onHoverLeave: () => void,
47 }): React$Node {
48 const store = useContext(StoreContext);
49 const inputRef = useRef();
50 function handleChange(event: SyntheticEvent) {
51 const newValue = +event.currentTarget.value;
52 onChange(newValue);
53 }
54 function handlePress(index: number, event: SyntheticEvent) {
55 event.preventDefault();
56 if (inputRef.current == null) {
57 throw new Error(
58 'The input should always be mounted while we can click things.',
59 );
60 }
61 inputRef.current.focus();
62 onChange(index);
63 }
64 const steps = [];
65 for (let index = min; index <= max; index++) {
66 const step = timeline[index];
67 const environment = step.environment;
68 const element = store.getElementByID(step.id);
69 const label =
70 index === min
71 ? // The first step in the timeline is always a Transition (Initial Paint).
72 element === null || element.type === ElementTypeRoot
73 ? 'Initial Paint'
74 : 'Transition' +
75 (environment === null ? '' : ' (' + environment + ')')
76 : // TODO: Consider adding the name of this specific boundary if this step has only one.
77 environment === null
78 ? 'Suspense'
79 : environment;
80 steps.push(
81 <Tooltip key={index} label={label}>
82 <div
83 className={
84 styles.SuspenseScrubberStep +
85 (highlight === index
86 ? ' ' + styles.SuspenseScrubberStepHighlight
87 : '')
88 }
89 onPointerDown={handlePress.bind(null, index)}
90 onMouseEnter={onHoverSegment.bind(null, index)}>
91 <div
92 className={
93 styles.SuspenseScrubberBead +
94 (index === min
95 ? // The first step in the timeline is always a Transition (Initial Paint).
96 ' ' + styles.SuspenseScrubberBeadTransition
97 : '') +
98 ' ' +
99 getClassNameForEnvironment(environment) +
100 (index <= value ? ' ' + styles.SuspenseScrubberBeadSelected : '')
101 }
102 />
103 </div>
104 </Tooltip>,
105 );
106 }
107
108 return (
109 <div className={styles.SuspenseScrubber} onMouseLeave={onHoverLeave}>
110 <input
111 className={styles.SuspenseScrubberInput}
112 type="range"
113 min={min}
114 max={max}
115 value={value}
116 onBlur={onBlur}
117 onChange={handleChange}
118 onFocus={onFocus}
119 ref={inputRef}
120 />
121 {steps}
122 </div>
123 );
124 }