Fix redraw after resizing elements
Summary: Fixed a longer standing issue where after a horizontal resize the rows wouldn't redraw until new data arrives (or the user scrolls), resulting in rendering artefacts. Also introduced a hook to force a reflow of contents if the contents of a hook changes. It's a bit leaky abstraction, but does keep the virtualization performant if dynamic heights are used. Reviewed By: passy Differential Revision: D27395516 fbshipit-source-id: 1691af3ec64f1a476969a318553d83e22239997c
This commit is contained in:
committed by
Facebook GitHub Bot
parent
b597da01e7
commit
8cd38a6b49
@@ -11,6 +11,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/css": "^11.1.3",
|
"@emotion/css": "^11.1.3",
|
||||||
"@emotion/react": "^11.1.5",
|
"@emotion/react": "^11.1.5",
|
||||||
|
"@reach/observe-rect": "^1.2.0",
|
||||||
"immer": "^9.0.0",
|
"immer": "^9.0.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"react-element-to-jsx-string": "^14.3.2",
|
"react-element-to-jsx-string": "^14.3.2",
|
||||||
|
|||||||
@@ -15,10 +15,11 @@ import {
|
|||||||
import {Button, Typography} from 'antd';
|
import {Button, Typography} from 'antd';
|
||||||
import {pad} from 'lodash';
|
import {pad} from 'lodash';
|
||||||
import React, {createElement, Fragment, isValidElement, useState} from 'react';
|
import React, {createElement, Fragment, isValidElement, useState} from 'react';
|
||||||
import styled from '@emotion/styled';
|
|
||||||
import {tryGetFlipperLibImplementation} from '../plugin/FlipperLib';
|
import {tryGetFlipperLibImplementation} from '../plugin/FlipperLib';
|
||||||
import {safeStringify} from '../utils/safeStringify';
|
import {safeStringify} from '../utils/safeStringify';
|
||||||
import {urlRegex} from '../utils/urlRegex';
|
import {urlRegex} from '../utils/urlRegex';
|
||||||
|
import {useTableRedraw} from './datatable/DataSourceRenderer';
|
||||||
|
import {theme} from './theme';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A Formatter is used to render an arbitrarily value to React. If a formatter returns 'undefined'
|
* A Formatter is used to render an arbitrarily value to React. If a formatter returns 'undefined'
|
||||||
@@ -136,7 +137,7 @@ export const DataFormatter = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const TruncateHelper = styled(function TruncateHelper({
|
function TruncateHelper({
|
||||||
value,
|
value,
|
||||||
maxLength,
|
maxLength,
|
||||||
}: {
|
}: {
|
||||||
@@ -144,29 +145,37 @@ const TruncateHelper = styled(function TruncateHelper({
|
|||||||
maxLength: number;
|
maxLength: number;
|
||||||
}) {
|
}) {
|
||||||
const [collapsed, setCollapsed] = useState(true);
|
const [collapsed, setCollapsed] = useState(true);
|
||||||
|
const redrawRow = useTableRedraw();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{collapsed ? value.substr(0, maxLength) : value}
|
{collapsed ? value.substr(0, maxLength) : value}
|
||||||
<Button
|
<Button
|
||||||
onClick={() => setCollapsed((c) => !c)}
|
onClick={() => {
|
||||||
|
setCollapsed((c) => !c);
|
||||||
|
redrawRow?.();
|
||||||
|
}}
|
||||||
size="small"
|
size="small"
|
||||||
type="text"
|
type="text"
|
||||||
|
style={truncateButtonStyle}
|
||||||
icon={collapsed ? <CaretRightOutlined /> : <CaretUpOutlined />}>
|
icon={collapsed ? <CaretRightOutlined /> : <CaretUpOutlined />}>
|
||||||
{`(and ${value.length - maxLength} more...)`}
|
{`(and ${value.length - maxLength} more...)`}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
icon={<CopyOutlined />}
|
icon={<CopyOutlined />}
|
||||||
onClick={() =>
|
onClick={() => {
|
||||||
tryGetFlipperLibImplementation()?.writeTextToClipboard(value)
|
tryGetFlipperLibImplementation()?.writeTextToClipboard(value);
|
||||||
}
|
}}
|
||||||
size="small"
|
size="small"
|
||||||
type="text">
|
type="text"
|
||||||
|
style={truncateButtonStyle}>
|
||||||
Copy
|
Copy
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
})({
|
}
|
||||||
'& button': {
|
|
||||||
marginRight: 4,
|
const truncateButtonStyle = {
|
||||||
},
|
color: theme.textColorPrimary,
|
||||||
});
|
marginLeft: 4,
|
||||||
|
};
|
||||||
|
|||||||
@@ -15,10 +15,13 @@ import React, {
|
|||||||
useState,
|
useState,
|
||||||
useLayoutEffect,
|
useLayoutEffect,
|
||||||
MutableRefObject,
|
MutableRefObject,
|
||||||
|
useContext,
|
||||||
|
createContext,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import {DataSource} from '../../state/DataSource';
|
import {DataSource} from '../../state/DataSource';
|
||||||
import {useVirtual} from 'react-virtual';
|
import {useVirtual} from 'react-virtual';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
import observeRect from '@reach/observe-rect';
|
||||||
|
|
||||||
// how fast we update if updates are low-prio (e.g. out of window and not super significant)
|
// how fast we update if updates are low-prio (e.g. out of window and not super significant)
|
||||||
const LOW_PRIO_UPDATE = 1000; //ms
|
const LOW_PRIO_UPDATE = 1000; //ms
|
||||||
@@ -92,9 +95,8 @@ export const DataSourceRenderer: <T extends object, C>(
|
|||||||
// render scheduling
|
// render scheduling
|
||||||
const renderPending = useRef(UpdatePrio.NONE);
|
const renderPending = useRef(UpdatePrio.NONE);
|
||||||
const lastRender = useRef(Date.now());
|
const lastRender = useRef(Date.now());
|
||||||
const setForceUpdate = useState(0)[1];
|
const [, setForceUpdate] = useState(0);
|
||||||
const forceHeightRecalculation = useRef(0);
|
const forceHeightRecalculation = useRef(0);
|
||||||
|
|
||||||
const parentRef = React.useRef<null | HTMLDivElement>(null);
|
const parentRef = React.useRef<null | HTMLDivElement>(null);
|
||||||
|
|
||||||
const virtualizer = useVirtual({
|
const virtualizer = useVirtual({
|
||||||
@@ -111,6 +113,11 @@ export const DataSourceRenderer: <T extends object, C>(
|
|||||||
virtualizerRef.current = virtualizer;
|
virtualizerRef.current = virtualizer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const redraw = useCallback(() => {
|
||||||
|
forceHeightRecalculation.current++;
|
||||||
|
setForceUpdate((x) => x + 1);
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
function subscribeToDataSource() {
|
function subscribeToDataSource() {
|
||||||
const forceUpdate = () => {
|
const forceUpdate = () => {
|
||||||
@@ -261,40 +268,64 @@ export const DataSourceRenderer: <T extends object, C>(
|
|||||||
lastRender.current = Date.now();
|
lastRender.current = Date.now();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observer parent height
|
||||||
|
*/
|
||||||
|
useEffect(
|
||||||
|
function redrawOnResize() {
|
||||||
|
if (!parentRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let lastWidth = 0;
|
||||||
|
const observer = observeRect(parentRef.current, (rect) => {
|
||||||
|
if (lastWidth !== rect.width) {
|
||||||
|
lastWidth = rect.width;
|
||||||
|
redraw();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
observer.observe();
|
||||||
|
return () => observer.unobserve();
|
||||||
|
},
|
||||||
|
[redraw],
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rendering
|
* Rendering
|
||||||
*/
|
*/
|
||||||
return (
|
return (
|
||||||
<TableContainer onScroll={onScroll} ref={parentRef}>
|
<RedrawContext.Provider value={redraw}>
|
||||||
{virtualizer.virtualItems.length === 0
|
<TableContainer onScroll={onScroll} ref={parentRef}>
|
||||||
? emptyRenderer?.(dataSource)
|
{virtualizer.virtualItems.length === 0
|
||||||
: null}
|
? emptyRenderer?.(dataSource)
|
||||||
<TableWindow
|
: null}
|
||||||
height={virtualizer.totalSize}
|
<TableWindow
|
||||||
onKeyDown={onKeyDown}
|
height={virtualizer.totalSize}
|
||||||
tabIndex={0}>
|
onKeyDown={onKeyDown}
|
||||||
{virtualizer.virtualItems.map((virtualRow) => {
|
tabIndex={0}>
|
||||||
const value = dataSource.view.get(virtualRow.index);
|
{virtualizer.virtualItems.map((virtualRow) => {
|
||||||
// the position properties always change, so they are not part of the TableRow to avoid invalidating the memoized render always.
|
const value = dataSource.view.get(virtualRow.index);
|
||||||
// Also all row containers are renderd as part of same component to have 'less react' framework code in between*/}
|
// the position properties always change, so they are not part of the TableRow to avoid invalidating the memoized render always.
|
||||||
return (
|
// Also all row containers are renderd as part of same component to have 'less react' framework code in between*/}
|
||||||
<div
|
return (
|
||||||
key={virtualRow.index}
|
<div
|
||||||
style={{
|
key={virtualRow.index}
|
||||||
position: 'absolute',
|
style={{
|
||||||
top: 0,
|
position: 'absolute',
|
||||||
left: 0,
|
top: 0,
|
||||||
width: '100%',
|
left: 0,
|
||||||
height: useFixedRowHeight ? virtualRow.size : undefined,
|
width: '100%',
|
||||||
transform: `translateY(${virtualRow.start}px)`,
|
height: useFixedRowHeight ? virtualRow.size : undefined,
|
||||||
}}
|
transform: `translateY(${virtualRow.start}px)`,
|
||||||
ref={useFixedRowHeight ? undefined : virtualRow.measureRef}>
|
}}
|
||||||
{itemRenderer(value, virtualRow.index, context)}
|
ref={useFixedRowHeight ? undefined : virtualRow.measureRef}>
|
||||||
</div>
|
{itemRenderer(value, virtualRow.index, context)}
|
||||||
);
|
</div>
|
||||||
})}
|
);
|
||||||
</TableWindow>
|
})}
|
||||||
</TableContainer>
|
</TableWindow>
|
||||||
|
</TableContainer>
|
||||||
|
</RedrawContext.Provider>
|
||||||
);
|
);
|
||||||
}) as any;
|
}) as any;
|
||||||
|
|
||||||
@@ -310,3 +341,9 @@ const TableWindow = styled.div<{height: number}>(({height}) => ({
|
|||||||
position: 'relative',
|
position: 'relative',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
export const RedrawContext = createContext<undefined | (() => void)>(undefined);
|
||||||
|
|
||||||
|
export function useTableRedraw() {
|
||||||
|
return useContext(RedrawContext);
|
||||||
|
}
|
||||||
|
|||||||
@@ -2150,7 +2150,7 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@oclif/screen/-/screen-1.0.4.tgz#b740f68609dfae8aa71c3a6cab15d816407ba493"
|
resolved "https://registry.yarnpkg.com/@oclif/screen/-/screen-1.0.4.tgz#b740f68609dfae8aa71c3a6cab15d816407ba493"
|
||||||
integrity sha512-60CHpq+eqnTxLZQ4PGHYNwUX572hgpMHGPtTWMjdTMsAvlm69lZV/4ly6O3sAYkomo4NggGcomrDpBe34rxUqw==
|
integrity sha512-60CHpq+eqnTxLZQ4PGHYNwUX572hgpMHGPtTWMjdTMsAvlm69lZV/4ly6O3sAYkomo4NggGcomrDpBe34rxUqw==
|
||||||
|
|
||||||
"@reach/observe-rect@^1.1.0":
|
"@reach/observe-rect@^1.1.0", "@reach/observe-rect@^1.2.0":
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/@reach/observe-rect/-/observe-rect-1.2.0.tgz#d7a6013b8aafcc64c778a0ccb83355a11204d3b2"
|
resolved "https://registry.yarnpkg.com/@reach/observe-rect/-/observe-rect-1.2.0.tgz#d7a6013b8aafcc64c778a0ccb83355a11204d3b2"
|
||||||
integrity sha512-Ba7HmkFgfQxZqqaeIWWkNK0rEhpxVQHIoVyW1YDSkGsGIXzcaW4deC8B0pZrNSSyLTdIk7y+5olKt5+g0GmFIQ==
|
integrity sha512-Ba7HmkFgfQxZqqaeIWWkNK0rEhpxVQHIoVyW1YDSkGsGIXzcaW4deC8B0pZrNSSyLTdIk7y+5olKt5+g0GmFIQ==
|
||||||
|
|||||||
Reference in New Issue
Block a user