New implementation of nested hover

Summary:
There were 2 issues with the previous implementation of the nested hover.
1. If you moved the mouse out of the inspector quickly we would miss the event and we would have a hover state of the root element when we shouldnt
2. The hover state was stored per node, it was possible to have mulitple children hovered at the same time if you moved the mouse fast enough in a very complex tree

The new implementation has the hovered id stored in the Datainspector root. This solves the multiple state issue since there can only be one.  Finally There is an onMouseLeave hook added to the parent div which seems to reliably fire no mouse how erractic my mouse movements :) Also the new implementation is a lot easier to understand

Reviewed By: mweststrate

Differential Revision: D39855733

fbshipit-source-id: 96b43f216deef72b81cd52001f8de26df55ea693
This commit is contained in:
Luke De Feo
2022-09-28 06:51:13 -07:00
committed by Facebook GitHub Bot
parent 21ca10a78c
commit 8f9ac0d087
2 changed files with 32 additions and 43 deletions

View File

@@ -82,6 +82,7 @@ type DataInspectorState = {
filterExpanded: DataInspectorExpanded; filterExpanded: DataInspectorExpanded;
userExpanded: DataInspectorExpanded; userExpanded: DataInspectorExpanded;
filter: string; filter: string;
hoveredNodePath: string | undefined;
}; };
const MAX_RESULTS = 50; const MAX_RESULTS = 50;
@@ -102,6 +103,7 @@ export class DataInspector extends PureComponent<
userExpanded: {}, userExpanded: {},
filterExpanded: {}, filterExpanded: {},
filter: '', filter: '',
hoveredNodePath: undefined,
}; };
static getDerivedStateFromProps( static getDerivedStateFromProps(
@@ -181,6 +183,16 @@ export class DataInspector extends PureComponent<
}); });
}; };
setHoveredNodePath = (path?: string) => {
this.setState({
hoveredNodePath: path,
});
};
removeHover = () => {
this.setHoveredNodePath(undefined);
};
// make sure this fn is a stable ref to not invalidate the whole tree on new data // make sure this fn is a stable ref to not invalidate the whole tree on new data
getRootData = () => { getRootData = () => {
return this.props.data; return this.props.data;
@@ -188,10 +200,12 @@ export class DataInspector extends PureComponent<
render() { render() {
return ( return (
<Layout.Container> <Layout.Container onMouseLeave={this.removeHover}>
<RootDataContext.Provider value={this.getRootData}> <RootDataContext.Provider value={this.getRootData}>
<HighlightProvider text={this.props.filter}> <HighlightProvider text={this.props.filter}>
<DataInspectorNode <DataInspectorNode
hoveredNodePath={this.state.hoveredNodePath}
setHoveredNodePath={this.setHoveredNodePath}
data={this.props.data} data={this.props.data}
diff={this.props.diff} diff={this.props.diff}
extractValue={this.props.extractValue} extractValue={this.props.extractValue}

View File

@@ -181,9 +181,9 @@ type DataInspectorProps = {
name?: string, name?: string,
) => ReactElement[]; ) => ReactElement[];
onMouseEnter?: () => void; hoveredNodePath?: string;
onMouseExit?: () => void; setHoveredNodePath: (path: string) => void;
}; };
const defaultValueExtractor: DataValueExtractor = (value: any) => { const defaultValueExtractor: DataValueExtractor = (value: any) => {
@@ -315,14 +315,13 @@ export const DataInspectorNode: React.FC<DataInspectorProps> = memo(
tooltips, tooltips,
setValue: setValueProp, setValue: setValueProp,
additionalContextMenuItems, additionalContextMenuItems,
onMouseEnter, hoveredNodePath,
onMouseExit, setHoveredNodePath,
}) { }) {
const highlighter = useHighlighter(); const highlighter = useHighlighter();
const getRoot = useContext(RootDataContext); const getRoot = useContext(RootDataContext);
const isUnitTest = useInUnitTest(); const isUnitTest = useInUnitTest();
const [hover, setHover] = useState(false);
const shouldExpand = useRef(false); const shouldExpand = useRef(false);
const expandHandle = useRef(undefined as any); const expandHandle = useRef(undefined as any);
const [renderExpanded, setRenderExpanded] = useState(false); const [renderExpanded, setRenderExpanded] = useState(false);
@@ -428,37 +427,6 @@ export const DataInspectorNode: React.FC<DataInspectorProps> = memo(
[onDelete], [onDelete],
); );
const thisOnMouseEnter = useCallback(
(e: SyntheticEvent) => {
onMouseEnter?.();
e.stopPropagation();
setHover(true);
},
[onMouseEnter],
);
const thisOnMouseLeave = useCallback(
(e: SyntheticEvent) => {
onMouseExit?.();
e.stopPropagation();
setHover(false);
},
[onMouseExit],
);
const childOnMouseEnter = useCallback(() => {
//when a child is hovered we are in both child and parents bounds so
//manually disable our own hover state so we focus on child
setHover(false);
}, []);
const childOnMouseExit = useCallback(() => {
//If a child has been unhovered then it means we have left the childs bounds and mouse should still be in parents
//(current element's) bounds. However the mouse enter callback wont fire again in this element since we never left the bounds.
//Therefore we manually set hovered back to true
setHover(true);
}, []);
/** /**
* RENDERING * RENDERING
*/ */
@@ -502,8 +470,8 @@ export const DataInspectorNode: React.FC<DataInspectorProps> = memo(
const metaKey = key + index; const metaKey = key + index;
const dataInspectorNode = ( const dataInspectorNode = (
<DataInspectorNode <DataInspectorNode
onMouseEnter={childOnMouseEnter} setHoveredNodePath={setHoveredNodePath}
onMouseExit={childOnMouseExit} hoveredNodePath={hoveredNodePath}
parentAncestry={ancestry} parentAncestry={ancestry}
extractValue={extractValue} extractValue={extractValue}
setValue={setValue} setValue={setValue}
@@ -662,13 +630,19 @@ export const DataInspectorNode: React.FC<DataInspectorProps> = memo(
); );
} }
const nodePath = path.join('.');
return ( return (
<Dropdown overlay={getContextMenu} trigger={contextMenuTrigger}> <Dropdown overlay={getContextMenu} trigger={contextMenuTrigger}>
<BaseContainer <BaseContainer
onContextMenu={stopPropagation} onContextMenu={stopPropagation}
hovered={hover} hovered={hoveredNodePath === nodePath}
onMouseEnter={thisOnMouseEnter} onMouseEnter={() => {
onMouseLeave={thisOnMouseLeave} setHoveredNodePath(nodePath);
}}
onMouseLeave={() => {
setHoveredNodePath(parentPath.join('.'));
}}
depth={depth} depth={depth}
disabled={!!setValueProp && !!setValue === false}> disabled={!!setValueProp && !!setValue === false}>
<PropertyContainer onClick={isExpandable ? handleClick : undefined}> <PropertyContainer onClick={isExpandable ? handleClick : undefined}>
@@ -755,7 +729,8 @@ function dataInspectorPropsAreEqual(
nextProps.onDelete === props.onDelete && nextProps.onDelete === props.onDelete &&
nextProps.setValue === props.setValue && nextProps.setValue === props.setValue &&
nextProps.collapsed === props.collapsed && nextProps.collapsed === props.collapsed &&
nextProps.expandRoot === props.expandRoot nextProps.expandRoot === props.expandRoot &&
nextProps.hoveredNodePath === props.hoveredNodePath
); );
} }