Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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(
<div
key={index}
className={styles.SuspenseScrubberStep}
onPointerDown={handlePress.bind(null, index)}
onMouseEnter={onHoverSegment.bind(null, index)}>
<div
className={
index <= value
? styles.SuspenseScrubberBeadSelected
: styles.SuspenseScrubberBead
}
/>
</div>,
);
}

return (
<div className={styles.SuspenseScrubber} onMouseLeave={onHoverLeave}>
<input
className={styles.SuspenseScrubberInput}
type="range"
min={min}
max={max}
value={value}
onBlur={onBlur}
onChange={handleChange}
onFocus={onFocus}
ref={inputRef}
/>
{steps}
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';

Expand All @@ -39,29 +36,6 @@ function SuspenseTimelineInput() {
playing,
} = useContext(SuspenseTreeStateContext);

const inputRef = useRef<HTMLElement | null>(null);
const inputBBox = useRef<ClientRect | null>(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;

Expand Down Expand Up @@ -100,8 +74,7 @@ function SuspenseTimelineInput() {
});
}

function handleChange(event: SyntheticEvent) {
const pendingTimelineIndex = +event.currentTarget.value;
function handleChange(pendingTimelineIndex: number) {
switchSuspenseNode(pendingTimelineIndex);
}

Expand All @@ -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);
Expand Down Expand Up @@ -239,18 +198,15 @@ function SuspenseTimelineInput() {
<div
className={styles.SuspenseTimelineInput}
title={timelineIndex + '/' + max}>
<input
className={styles.SuspenseTimelineSlider}
type="range"
<SuspenseScrubber
min={min}
max={max}
value={timelineIndex}
onBlur={handleBlur}
onChange={handleChange}
onFocus={handleFocus}
onPointerMove={handlePointerMove}
onPointerUp={clearHighlightHostInstance}
ref={inputRef}
onHoverSegment={handleHoverSegment}
onHoverLeave={clearHighlightHostInstance}
/>
</div>
</>
Expand Down
Loading