diff --git a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseScrubber.css b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseScrubber.css new file mode 100644 index 0000000000000..94e51ef63d330 --- /dev/null +++ b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseScrubber.css @@ -0,0 +1,57 @@ +.SuspenseScrubber { + position: relative; + width: 100%; + height: 1.5rem; + border-radius: 0.75rem; + padding: 0.25rem; + box-sizing: border-box; + display: flex; + align-items: center; +} + +.SuspenseScrubber:has(.SuspenseScrubberInput:focus-visible) { + outline: 2px solid var(--color-button-background-focus); +} + +.SuspenseScrubberInput { + position: absolute; + width: 100%; + opacity: 0; + height: 0px; + overflow: hidden; +} + +.SuspenseScrubberInput:focus { + outline: none; +} + +.SuspenseScrubberStep { + cursor: pointer; + flex: 1; + height: 100%; + padding-right: 1px; /* we use this instead of flex gap to make every pixel clickable */ + display: flex; + align-items: center; +} +.SuspenseScrubberStep:last-child { + padding-right: 0; +} + +.SuspenseScrubberBead, .SuspenseScrubberBeadSelected { + flex: 1; + height: 0.5rem; + background: var(--color-background-selected); + border-radius: 0.5rem; + background: var(--color-selected-tree-highlight-active); + transition: all 0.3s ease-in-out; +} + +.SuspenseScrubberBeadSelected { + height: 1rem; + background: var(--color-background-selected); +} + +.SuspenseScrubberStep:hover > .SuspenseScrubberBead, +.SuspenseScrubberStep:hover > .SuspenseScrubberBeadSelected { + height: 0.75rem; +} diff --git a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseScrubber.js b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseScrubber.js new file mode 100644 index 0000000000000..f1f96a33e00e2 --- /dev/null +++ b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseScrubber.js @@ -0,0 +1,86 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import typeof {SyntheticEvent} from 'react-dom-bindings/src/events/SyntheticEvent'; + +import * as React from 'react'; +import {useRef} from 'react'; + +import styles from './SuspenseScrubber.css'; + +export default function SuspenseScrubber({ + min, + max, + value, + onBlur, + onChange, + onFocus, + onHoverSegment, + onHoverLeave, +}: { + min: number, + max: number, + value: number, + onBlur: () => void, + onChange: (index: number) => void, + onFocus: () => void, + onHoverSegment: (index: number) => void, + onHoverLeave: () => void, +}): React$Node { + const inputRef = useRef(); + function handleChange(event: SyntheticEvent) { + const newValue = +event.currentTarget.value; + onChange(newValue); + } + function handlePress(index: number, event: SyntheticEvent) { + event.preventDefault(); + if (inputRef.current == null) { + throw new Error( + 'The input should always be mounted while we can click things.', + ); + } + inputRef.current.focus(); + onChange(index); + } + const steps = []; + for (let index = min; index <= max; index++) { + steps.push( +
+
+
, + ); + } + + return ( +
+ + {steps} +
+ ); +} diff --git a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTimeline.css b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTimeline.css index 08e7723ec0de4..77bb01c5fa32b 100644 --- a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTimeline.css +++ b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTimeline.css @@ -9,11 +9,6 @@ display: flex; flex-direction: column; flex-grow: 1; - /* - * `overflow: auto` will add scrollbars but the input will not actually grow beyond visible content. - * `overflow: hidden` will constrain the input to its visible content. - */ - overflow: hidden; } .SuspenseTimelineRootSwitcher { diff --git a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTimeline.js b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTimeline.js index 59bc6c8506308..d618bfcea63b3 100644 --- a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTimeline.js +++ b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTimeline.js @@ -8,7 +8,7 @@ */ import * as React from 'react'; -import {useContext, useLayoutEffect, useEffect, useRef} from 'react'; +import {useContext, useEffect} from 'react'; import {BridgeContext, StoreContext} from '../context'; import {TreeDispatcherContext} from '../Components/TreeContext'; import {useHighlightHostInstance} from '../hooks'; @@ -17,10 +17,7 @@ import { SuspenseTreeStateContext, } from './SuspenseTreeContext'; import styles from './SuspenseTimeline.css'; -import typeof { - SyntheticEvent, - SyntheticPointerEvent, -} from 'react-dom-bindings/src/events/SyntheticEvent'; +import SuspenseScrubber from './SuspenseScrubber'; import Button from '../Button'; import ButtonIcon from '../ButtonIcon'; @@ -39,29 +36,6 @@ function SuspenseTimelineInput() { playing, } = useContext(SuspenseTreeStateContext); - const inputRef = useRef(null); - const inputBBox = useRef(null); - useLayoutEffect(() => { - if (timeline.length === 0) { - return; - } - - const input = inputRef.current; - if (input === null) { - throw new Error('Expected an input HTML element to be present.'); - } - - inputBBox.current = input.getBoundingClientRect(); - const observer = new ResizeObserver(entries => { - inputBBox.current = input.getBoundingClientRect(); - }); - observer.observe(input); - return () => { - inputBBox.current = null; - observer.disconnect(); - }; - }, [timeline.length]); - const min = 0; const max = timeline.length > 0 ? timeline.length - 1 : 0; @@ -100,8 +74,7 @@ function SuspenseTimelineInput() { }); } - function handleChange(event: SyntheticEvent) { - const pendingTimelineIndex = +event.currentTarget.value; + function handleChange(pendingTimelineIndex: number) { switchSuspenseNode(pendingTimelineIndex); } @@ -113,25 +86,11 @@ function SuspenseTimelineInput() { switchSuspenseNode(timelineIndex); } - function handlePointerMove(event: SyntheticPointerEvent) { - const bbox = inputBBox.current; - if (bbox === null) { - throw new Error('Bounding box of slider is unknown.'); - } - - const hoveredValue = Math.max( - min, - Math.min( - Math.round( - min + ((event.clientX - bbox.left) / bbox.width) * (max - min), - ), - max, - ), - ); + function handleHoverSegment(hoveredValue: number) { const suspenseID = timeline[hoveredValue]; if (suspenseID === undefined) { throw new Error( - `Suspense node not found for value ${hoveredValue} in timeline when on ${event.clientX} in bounding box ${JSON.stringify(bbox)}.`, + `Suspense node not found for value ${hoveredValue} in timeline.`, ); } highlightHostInstance(suspenseID); @@ -239,18 +198,15 @@ function SuspenseTimelineInput() {
-