/
githubmr
/
facebook-react
Обзор
Документация
Войти
/
githubmr
/
facebook-react
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
main
packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTimeline.js
200 строк
6 KB
Sebastian Markbåge
[DevTools] Explicitly say which id to scroll to and only once (#34823)
13 окт 2025, 19:09
Не верифицирован
13 окт 2025, 19:09
b467c6e
Код
Авторство
О чём код?
/** * 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 * as React from 'react'; import {useContext, useEffect} from 'react'; import {BridgeContext} from '../context'; import {TreeDispatcherContext} from '../Components/TreeContext'; import {useHighlightHostInstance, useScrollToHostInstance} from '../hooks'; import { SuspenseTreeDispatcherContext, SuspenseTreeStateContext, } from './SuspenseTreeContext'; import styles from './SuspenseTimeline.css'; import SuspenseScrubber from './SuspenseScrubber'; import Button from '../Button'; import ButtonIcon from '../ButtonIcon'; function SuspenseTimelineInput() { const bridge = useContext(BridgeContext); const treeDispatch = useContext(TreeDispatcherContext); const suspenseTreeDispatch = useContext(SuspenseTreeDispatcherContext); const {highlightHostInstance, clearHighlightHostInstance} = useHighlightHostInstance(); const scrollToHostInstance = useScrollToHostInstance(); const {timeline, timelineIndex, hoveredTimelineIndex, playing, autoScroll} = useContext(SuspenseTreeStateContext); const min = 0; const max = timeline.length > 0 ? timeline.length - 1 : 0; function switchSuspenseNode(nextTimelineIndex: number) { const nextSelectedSuspenseID = timeline[nextTimelineIndex]; highlightHostInstance(nextSelectedSuspenseID); treeDispatch({ type: 'SELECT_ELEMENT_BY_ID', payload: nextSelectedSuspenseID, }); suspenseTreeDispatch({ type: 'SUSPENSE_SET_TIMELINE_INDEX', payload: nextTimelineIndex, }); } function handleChange(pendingTimelineIndex: number) { switchSuspenseNode(pendingTimelineIndex); } function handleBlur() { clearHighlightHostInstance(); } function handleFocus() { switchSuspenseNode(timelineIndex); } function handleHoverSegment(hoveredValue: number) { const suspenseID = timeline[hoveredValue]; if (suspenseID === undefined) { throw new Error( `Suspense node not found for value ${hoveredValue} in timeline.`, ); } highlightHostInstance(suspenseID); } function skipPrevious() { const nextSelectedSuspenseID = timeline[timelineIndex - 1]; treeDispatch({ type: 'SELECT_ELEMENT_BY_ID', payload: nextSelectedSuspenseID, }); suspenseTreeDispatch({ type: 'SUSPENSE_SKIP_TIMELINE_INDEX', payload: false, }); } function skipForward() { const nextSelectedSuspenseID = timeline[timelineIndex + 1]; treeDispatch({ type: 'SELECT_ELEMENT_BY_ID', payload: nextSelectedSuspenseID, }); suspenseTreeDispatch({ type: 'SUSPENSE_SKIP_TIMELINE_INDEX', payload: true, }); } function togglePlaying() { suspenseTreeDispatch({ type: 'SUSPENSE_PLAY_PAUSE', payload: 'toggle', }); } // TODO: useEffectEvent here once it's supported in all versions DevTools supports. // For now we just exclude it from deps since we don't lint those anyway. function changeTimelineIndex(newIndex: number) { // Synchronize timeline index with what is resuspended. // We suspend everything after the current selection. The root isn't showing // anything suspended in the root. The step after that should have one less // thing suspended. I.e. the first suspense boundary should be unsuspended // when it's selected. This also lets you show everything in the last step. const suspendedSet = timeline.slice(timelineIndex + 1); bridge.send('overrideSuspenseMilestone', { suspendedSet, }); } useEffect(() => { changeTimelineIndex(timelineIndex); }, [timelineIndex]); useEffect(() => { if (autoScroll.id > 0) { const scrollToId = autoScroll.id; // Consume the scroll ref so that we only trigger this scroll once. autoScroll.id = 0; scrollToHostInstance(scrollToId); } }, [autoScroll]); useEffect(() => { if (!playing) { return undefined; } // While playing, advance one step every second. const PLAY_SPEED_INTERVAL = 1000; const timer = setInterval(() => { suspenseTreeDispatch({ type: 'SUSPENSE_PLAY_TICK', }); }, PLAY_SPEED_INTERVAL); return () => { clearInterval(timer); }; }, [playing]); if (timeline.length === 0) { return ( <div className={styles.SuspenseTimelineInput}> Root contains no Suspense nodes. </div> ); } return ( <> <Button disabled={timelineIndex === 0} title={'Previous'} onClick={skipPrevious}> <ButtonIcon type={'skip-previous'} /> </Button> <Button disabled={max === 0 && !playing} title={playing ? 'Pause' : 'Play'} onClick={togglePlaying}> <ButtonIcon type={playing ? 'pause' : 'play'} /> </Button> <Button disabled={timelineIndex === max} title={'Next'} onClick={skipForward}> <ButtonIcon type={'skip-next'} /> </Button> <div className={styles.SuspenseTimelineInput} title={timelineIndex + '/' + max}> <SuspenseScrubber min={min} max={max} value={timelineIndex} highlight={hoveredTimelineIndex} onBlur={handleBlur} onChange={handleChange} onFocus={handleFocus} onHoverSegment={handleHoverSegment} onHoverLeave={clearHighlightHostInstance} /> </div> </> ); } export default function SuspenseTimeline(): React$Node { return ( <div className={styles.SuspenseTimelineContainer}> <SuspenseTimelineInput /> </div> ); }