Skip to content
Open
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
80 changes: 30 additions & 50 deletions src/components/shared/WithSize.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,76 +2,56 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import * as React from 'react';
import { findDOMNode } from 'react-dom';
import type { CssPixels } from 'firefox-profiler/types';
import { getResizeObserverWrapper } from 'firefox-profiler/utils/resize-observer-wrapper';

type State = {
type SizeState = {
width: CssPixels;
height: CssPixels;
};

export type SizeProps = Readonly<State>;
export type SizeProps = Readonly<SizeState>;

export type PropsWithSize<Props> = Props & SizeProps;

/**
* Wraps a React component and makes 'width' and 'height' available in the
* wrapped component's props. These props start out at zero and are updated to
* the component's DOM node's getBoundingClientRect().width/.height after the
* component has been mounted. They also get updated when the window is
* resized.
*
* Note that the props are *not* updated if the size of the element changes
* for reasons other than a window resize.
* component has been mounted. They also get updated whenever the element's
* size changes.
*/
export function withSize<Props>(
Wrapped: React.ComponentType<PropsWithSize<Props>>
): React.ComponentType<Props> {
return class WithSizeWrapper extends React.PureComponent<Props, State> {
override state = { width: 0, height: 0 };
_container: HTMLElement | null = null;

override componentDidMount() {
const container = findDOMNode(this) as HTMLElement; // eslint-disable-line react/no-find-dom-node
if (!container) {
throw new Error('Unable to find the DOMNode');
return function WithSizeWrapper(props: Props) {
const [size, setSize] = React.useState<SizeState>({ width: 0, height: 0 });
const wrapperRef = React.useRef<HTMLDivElement>(null);

React.useEffect(() => {
const wrapper = wrapperRef.current;
if (!wrapper) {
return undefined;
}
this._container = container;
getResizeObserverWrapper().subscribe(container, this._resizeListener);
}

// The listener is only called when the document is visible.
_resizeListener = (contentRect: DOMRectReadOnly) => {
const container = this._container;
if (!container) {
return;
const child = wrapper.firstElementChild as HTMLElement | null;
if (!child) {
throw new Error(
'WithSize: the wrapped component must render a DOM element as its root.'
);
}
this._updateSize(container, contentRect);
};

override componentWillUnmount() {
const container = this._container;
if (container) {
getResizeObserverWrapper().unsubscribe(container, this._resizeListener);
}

this._container = null;
}

_updateSize(_container: HTMLElement, contentRect: DOMRectReadOnly) {
this.setState({
width: contentRect.width,
height: contentRect.height,
});
}

override render() {
const combinedProps: Props & SizeProps = {
...this.props,
...this.state,
const listener = (contentRect: DOMRectReadOnly) => {
setSize({ width: contentRect.width, height: contentRect.height });
};
return <Wrapped {...combinedProps} />;
}
getResizeObserverWrapper().subscribe(child, listener);
return () => {
getResizeObserverWrapper().unsubscribe(child, listener);
};
}, []);

return (
<div ref={wrapperRef} style={{ display: 'contents' }}>
<Wrapped {...props} {...size} />
</div>
);
};
}
10 changes: 2 additions & 8 deletions src/test/components/EmptyThreadIndicator.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,14 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import ReactDOM from 'react-dom';

import { render } from 'firefox-profiler/test/fixtures/testing-library';
import {
EmptyThreadIndicator,
getIndicatorPositions,
} from '../../components/timeline/EmptyThreadIndicator';
import { getProfileFromTextSamples } from '../fixtures/profiles/processed-profile';
import { mockRaf } from '../fixtures/mocks/request-animation-frame';
import { getElementWithFixedSize } from '../fixtures/mocks/element-size';
import { autoMockElementSize } from '../fixtures/mocks/element-size';

import type { StartEndRange } from 'firefox-profiler/types';

Expand All @@ -20,11 +18,7 @@ describe('EmptyThreadIndicator', function () {
const width = 100;
const height = 10;

beforeEach(() => {
jest
.spyOn(ReactDOM, 'findDOMNode')
.mockImplementation(() => getElementWithFixedSize({ width, height }));
});
autoMockElementSize({ width, height });

const { derivedThreads } = getProfileFromTextSamples(`
A A A
Expand Down
14 changes: 1 addition & 13 deletions src/test/components/Timeline.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,18 +33,14 @@ import {
import { autoMockCanvasContext } from '../fixtures/mocks/canvas-context';
import { autoMockDomRect } from 'firefox-profiler/test/fixtures/mocks/domrect';
import { mockRaf } from '../fixtures/mocks/request-animation-frame';
import {
autoMockElementSize,
getElementWithFixedSize,
} from '../fixtures/mocks/element-size';
import { autoMockElementSize } from '../fixtures/mocks/element-size';
import {
getMouseEvent,
fireFullClick,
fireFullKeyPress,
fireFullContextMenu,
type FakeMouseEventInit,
} from '../fixtures/utils';
import ReactDOM from 'react-dom';
import {
getProfileWithNiceTracks,
getProfileWithMoreNiceTracks,
Expand Down Expand Up @@ -1234,14 +1230,6 @@ function _getProfileWithDroppedSamples(): Profile {
}

describe('Timeline', function () {
beforeEach(() => {
jest
.spyOn(ReactDOM, 'findDOMNode')
.mockImplementation(() =>
getElementWithFixedSize({ width: 200, height: 300 })
);
});

it('displays a context menu when right clicking global and local tracks', () => {
const profile = getProfileWithNiceTracks();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,33 @@

exports[`EmptyThreadIndicator rendering matches the snapshot when rendering all three types of indicators 1`] = `
<div
class="timelineEmptyThreadIndicator"
style="display: contents;"
>
<div
class="timelineEmptyThreadIndicatorBlock timelineEmptyThreadIndicatorStartup"
style="left: 0px; width: 30px;"
/>
<div
class="timelineEmptyThreadIndicatorBlock timelineEmptyThreadIndicatorShutdown"
style="right: 0px; width: 10px;"
/>
<div
class="timelineEmptyThreadIndicatorBlock timelineEmptyThreadIndicatorEmptyBuffer"
style="left: 30px; width: 20px;"
/>
class="timelineEmptyThreadIndicator"
>
<div
class="timelineEmptyThreadIndicatorBlock timelineEmptyThreadIndicatorStartup"
style="left: 0px; width: 30px;"
/>
<div
class="timelineEmptyThreadIndicatorBlock timelineEmptyThreadIndicatorShutdown"
style="right: 0px; width: 10px;"
/>
<div
class="timelineEmptyThreadIndicatorBlock timelineEmptyThreadIndicatorEmptyBuffer"
style="left: 30px; width: 20px;"
/>
</div>
</div>
`;

exports[`EmptyThreadIndicator rendering matches the snapshot when rendering no indicators 1`] = `
<div
class="timelineEmptyThreadIndicator"
/>
style="display: contents;"
>
<div
class="timelineEmptyThreadIndicator"
/>
</div>
`;
Loading
Loading