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
4 changes: 2 additions & 2 deletions packages/react-devtools-shared/src/__tests__/bridge-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ describe('Bridge', () => {
// Check that we're wired up correctly.
bridge.send('reloadAppForProfiling');
jest.runAllTimers();
expect(wall.send).toHaveBeenCalledWith('reloadAppForProfiling');
expect(wall.send).toHaveBeenCalledWith('reloadAppForProfiling', undefined);

// Should flush pending messages and then shut down.
wall.send.mockClear();
Expand All @@ -37,7 +37,7 @@ describe('Bridge', () => {
jest.runAllTimers();
expect(wall.send).toHaveBeenCalledWith('update', '1');
expect(wall.send).toHaveBeenCalledWith('update', '2');
expect(wall.send).toHaveBeenCalledWith('shutdown');
expect(wall.send).toHaveBeenCalledWith('shutdown', undefined);
expect(shutdownCallback).toHaveBeenCalledTimes(1);

// Verify that the Bridge doesn't send messages after shutdown.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,66 @@ export default function setupHighlighter(
bridge.addListener('shutdown', stopInspectingHost);
bridge.addListener('startInspectingHost', startInspectingHost);
bridge.addListener('stopInspectingHost', stopInspectingHost);
bridge.addListener('scrollTo', scrollDocumentTo);
bridge.addListener('requestScrollPosition', sendScroll);

let applyingScroll = false;

function scrollDocumentTo({
left,
top,
right,
bottom,
}: {
left: number,
top: number,
right: number,
bottom: number,
}) {
if (
left === Math.round(window.scrollX) &&
top === Math.round(window.scrollY)
) {
return;
}
applyingScroll = true;
window.scrollTo({
top: top,
left: left,
behavior: 'smooth',
});
}

let scrollTimer = null;
function sendScroll() {
if (scrollTimer) {
clearTimeout(scrollTimer);
scrollTimer = null;
}
if (applyingScroll) {
return;
}
const left = window.scrollX;
const top = window.scrollY;
const right = left + window.innerWidth;
const bottom = top + window.innerHeight;
bridge.send('scrollTo', {left, top, right, bottom});
}

function scrollEnd() {
// Upon scrollend send it immediately.
sendScroll();
applyingScroll = false;
}

document.addEventListener('scroll', () => {
if (!scrollTimer) {
// Periodically synchronize the scroll while scrolling.
scrollTimer = setTimeout(sendScroll, 400);
}
});

document.addEventListener('scrollend', scrollEnd);

function startInspectingHost(onlySuspenseNodes: boolean) {
inspectOnlySuspenseNodes = onlySuspenseNodes;
Expand Down
6 changes: 5 additions & 1 deletion packages/react-devtools-shared/src/bridge.js
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ export type BackendEvents = {
selectElement: [number],
shutdown: [],
stopInspectingHost: [boolean],
scrollTo: [{left: number, top: number, right: number, bottom: number}],
syncSelectionToBuiltinElementsPanel: [],
unsupportedRendererVersion: [],

Expand Down Expand Up @@ -270,6 +271,8 @@ type FrontendEvents = {
startProfiling: [StartProfilingParams],
stopInspectingHost: [],
scrollToHostInstance: [ScrollToHostInstance],
scrollTo: [{left: number, top: number, right: number, bottom: number}],
requestScrollPosition: [],
stopProfiling: [],
storeAsGlobal: [StoreAsGlobalParams],
updateComponentFilters: [Array<ComponentFilter>],
Expand Down Expand Up @@ -416,7 +419,8 @@ class Bridge<
try {
if (this._messageQueue.length) {
for (let i = 0; i < this._messageQueue.length; i += 2) {
this._wall.send(this._messageQueue[i], ...this._messageQueue[i + 1]);
// This only supports one argument in practice but the types suggests it should support multiple.
this._wall.send(this._messageQueue[i], this._messageQueue[i + 1][0]);
}
this._messageQueue.length = 0;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import typeof {
} from 'react-dom-bindings/src/events/SyntheticEvent';

import * as React from 'react';
import {createContext, useContext} from 'react';
import {createContext, useContext, useLayoutEffect} from 'react';
import {
TreeDispatcherContext,
TreeStateContext,
Expand Down Expand Up @@ -435,7 +435,11 @@ function SuspenseRectsRoot({rootID}: {rootID: SuspenseNode['id']}): React$Node {

const ViewBox = createContext<Rect>((null: any));

function SuspenseRectsContainer(): React$Node {
function SuspenseRectsContainer({
scaleRef,
}: {
scaleRef: {current: number},
}): React$Node {
const store = useContext(StoreContext);
const {inspectedElementID} = useContext(TreeStateContext);
const treeDispatch = useContext(TreeDispatcherContext);
Expand Down Expand Up @@ -505,6 +509,11 @@ function SuspenseRectsContainer(): React$Node {
const rootEnvironment =
timeline.length === 0 ? null : timeline[0].environment;

useLayoutEffect(() => {
// 100% of the width represents this many pixels in the real document.
scaleRef.current = boundingBoxWidth;
}, [boundingBoxWidth]);

return (
<div
className={
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import {
SuspenseTreeDispatcherContext,
SuspenseTreeStateContext,
} from './SuspenseTreeContext';
import {StoreContext, OptionsContext} from '../context';
import {BridgeContext, StoreContext, OptionsContext} from '../context';
import Button from '../Button';
import Toggle from '../Toggle';
import typeof {SyntheticPointerEvent} from 'react-dom-bindings/src/events/SyntheticEvent';
Expand Down Expand Up @@ -157,6 +157,119 @@ function ToggleInspectedElement({
);
}

function SynchronizedScrollContainer({
className,
children,
scaleRef,
}: {
className?: string,
children?: React.Node,
scaleRef: {current: number},
}) {
const bridge = useContext(BridgeContext);
const ref = useRef(null);
const applyingScrollRef = useRef(false);

// TODO: useEffectEvent
function scrollContainerTo({
left,
top,
right,
bottom,
}: {
left: number,
top: number,
right: number,
bottom: number,
}): void {
const element = ref.current;
if (element === null) {
return;
}
const scale = scaleRef.current / element.clientWidth;
const targetLeft = Math.round(left / scale);
const targetTop = Math.round(top / scale);
if (
targetLeft !== Math.round(element.scrollLeft) ||
targetTop !== Math.round(element.scrollTop)
) {
// Disable scroll events until we've applied the new scroll position.
applyingScrollRef.current = true;
element.scrollTo({
left: targetLeft,
top: targetTop,
behavior: 'smooth',
});
}
}

useEffect(() => {
const callback = scrollContainerTo;
bridge.addListener('scrollTo', callback);
// Ask for the current scroll position when we mount so we can attach ourselves to it.
bridge.send('requestScrollPosition');
return () => bridge.removeListener('scrollTo', callback);
}, [bridge]);

const scrollTimer = useRef<null | TimeoutID>(null);

// TODO: useEffectEvent
function sendScroll() {
if (scrollTimer.current) {
clearTimeout(scrollTimer.current);
scrollTimer.current = null;
}
if (applyingScrollRef.current) {
return;
}
const element = ref.current;
if (element === null) {
return;
}
const scale = scaleRef.current / element.clientWidth;
const left = element.scrollLeft * scale;
const top = element.scrollTop * scale;
const right = left + element.clientWidth * scale;
const bottom = top + element.clientHeight * scale;
bridge.send('scrollTo', {left, top, right, bottom});
}

// TODO: useEffectEvent
function throttleScroll() {
if (!scrollTimer.current) {
// Periodically synchronize the scroll while scrolling.
scrollTimer.current = setTimeout(sendScroll, 400);
}
}

function scrollEnd() {
// Upon scrollend send it immediately.
sendScroll();
applyingScrollRef.current = false;
}

useEffect(() => {
const element = ref.current;
if (element === null) {
return;
}
const scrollCallback = throttleScroll;
const scrollEndCallback = scrollEnd;
element.addEventListener('scroll', scrollCallback);
element.addEventListener('scrollend', scrollEndCallback);
return () => {
element.removeEventListener('scroll', scrollCallback);
element.removeEventListener('scrollend', scrollEndCallback);
};
}, [ref]);

return (
<div className={className} ref={ref}>
{children}
</div>
);
}

function SuspenseTab(_: {}) {
const store = useContext(StoreContext);
const {hideSettings} = useContext(OptionsContext);
Expand Down Expand Up @@ -341,6 +454,8 @@ function SuspenseTab(_: {}) {
}
};

const scaleRef = useRef(0);

return (
<SettingsModalContextController>
<div className={styles.SuspenseTab} ref={wrapperTreeRef}>
Expand Down Expand Up @@ -388,9 +503,11 @@ function SuspenseTab(_: {}) {
orientation="horizontal"
/>
</header>
<div className={styles.Rects}>
<SuspenseRects />
</div>
<SynchronizedScrollContainer
className={styles.Rects}
scaleRef={scaleRef}>
<SuspenseRects scaleRef={scaleRef} />
</SynchronizedScrollContainer>
<footer className={styles.SuspenseTreeViewFooter}>
<SuspenseTimeline />
<div className={styles.SuspenseTreeViewFooterButtons}>
Expand Down
Loading