From 8f855d0c184646697f7f876ddff85f84bd6c443c Mon Sep 17 00:00:00 2001
From: joshwooding <12938082+joshwooding@users.noreply.github.com>
Date: Sun, 21 Jun 2020 16:21:22 +0100
Subject: [PATCH 1/8] First step
---
.../material-ui-lab/src/TreeItem/TreeItem.js | 21 +-
.../material-ui-lab/src/TreeView/TreeView.js | 31 ++-
.../src/TreeView/descendants.js | 224 ++++++++++++++++++
3 files changed, 255 insertions(+), 21 deletions(-)
create mode 100644 packages/material-ui-lab/src/TreeView/descendants.js
diff --git a/packages/material-ui-lab/src/TreeItem/TreeItem.js b/packages/material-ui-lab/src/TreeItem/TreeItem.js
index a79cd986b94f2f..b81b39870931ee 100644
--- a/packages/material-ui-lab/src/TreeItem/TreeItem.js
+++ b/packages/material-ui-lab/src/TreeItem/TreeItem.js
@@ -7,6 +7,7 @@ import Collapse from '@material-ui/core/Collapse';
import { fade, withStyles, useTheme } from '@material-ui/core/styles';
import { useForkRef } from '@material-ui/core/utils';
import TreeViewContext from '../TreeView/TreeViewContext';
+import { useDescendant } from '../TreeView/descendants';
export const styles = (theme) => ({
/* Styles applied to the root element. */
@@ -135,14 +136,18 @@ const TreeItem = React.forwardRef(function TreeItem(props, ref) {
multiSelect,
getParent,
mapFirstChar,
- addNodeToNodeMap,
- removeNodeFromNodeMap,
+ registerNode,
+ unregisterNode,
} = React.useContext(TreeViewContext);
const nodeRef = React.useRef(null);
const contentRef = React.useRef(null);
const handleRef = useForkRef(nodeRef, ref);
+ const index = useDescendant({
+ element: nodeRef.current,
+ });
+
let icon = iconProp;
const expandable = Boolean(Array.isArray(children) ? children.length : children);
@@ -337,25 +342,25 @@ const TreeItem = React.forwardRef(function TreeItem(props, ref) {
};
React.useEffect(() => {
- if (addNodeToNodeMap) {
+ if (registerNode) {
const childIds = [];
React.Children.forEach(children, (child) => {
if (React.isValidElement(child) && child.props.nodeId) {
childIds.push(child.props.nodeId);
}
});
- addNodeToNodeMap(nodeId, childIds);
+ registerNode(nodeId, childIds);
}
- }, [children, nodeId, addNodeToNodeMap]);
+ }, [children, nodeId, registerNode]);
React.useEffect(() => {
- if (removeNodeFromNodeMap) {
+ if (unregisterNode) {
return () => {
- removeNodeFromNodeMap(nodeId);
+ unregisterNode(nodeId);
};
}
return undefined;
- }, [nodeId, removeNodeFromNodeMap]);
+ }, [nodeId, unregisterNode]);
React.useEffect(() => {
if (mapFirstChar && label) {
diff --git a/packages/material-ui-lab/src/TreeView/TreeView.js b/packages/material-ui-lab/src/TreeView/TreeView.js
index bd64ce8ae6fcee..572a45acf70f55 100644
--- a/packages/material-ui-lab/src/TreeView/TreeView.js
+++ b/packages/material-ui-lab/src/TreeView/TreeView.js
@@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
import { withStyles } from '@material-ui/core/styles';
import { useControlled } from '@material-ui/core/utils';
import TreeViewContext from './TreeViewContext';
+import { DescendantProvider, useDescendantsInit } from './descendants';
export const styles = {
/* Styles applied to the root element. */
@@ -395,7 +396,7 @@ const TreeView = React.forwardRef(function TreeView(props, ref) {
* Mapping Helpers
*/
- const addNodeToNodeMap = (id, childrenIds) => {
+ const registerNode = (id, childrenIds) => {
const currentMap = nodeMap.current[id];
nodeMap.current[id] = { ...currentMap, children: childrenIds, id };
@@ -430,7 +431,7 @@ const TreeView = React.forwardRef(function TreeView(props, ref) {
firstCharMap.current = newMap;
}, []);
- const removeNodeFromNodeMap = React.useCallback(
+ const unregisterNode = React.useCallback(
(id) => {
const nodes = getNodesToRemove(id);
cleanUpFirstCharMap(nodes);
@@ -513,6 +514,8 @@ const TreeView = React.forwardRef(function TreeView(props, ref) {
return false;
};
+ const [descendants, setDescendants] = useDescendantsInit();
+
return (
-
+
+
+
);
});
diff --git a/packages/material-ui-lab/src/TreeView/descendants.js b/packages/material-ui-lab/src/TreeView/descendants.js
new file mode 100644
index 00000000000000..76d90c413be508
--- /dev/null
+++ b/packages/material-ui-lab/src/TreeView/descendants.js
@@ -0,0 +1,224 @@
+import * as React from 'react';
+import PropTypes from 'prop-types';
+
+// To replace with .findIndex() once we stop IE 11 support.
+function findIndex(array, comp) {
+ for (let i = 0; i < array.length; i += 1) {
+ if (comp(array[i])) {
+ return i;
+ }
+ }
+
+ return -1;
+}
+
+const useEnhancedEffect =
+ typeof window !== 'undefined' && process.env.NODE_ENV !== 'test'
+ ? React.useLayoutEffect
+ : React.useEffect;
+
+const DescendantContext = React.createContext({});
+
+if (process.env.NODE_ENV !== 'production') {
+ DescendantContext.displayName = 'DescendantContext';
+}
+
+function usePrevious(value) {
+ const ref = React.useRef(null);
+ React.useEffect(() => {
+ ref.current = value;
+ }, [value]);
+ return ref.current;
+}
+
+const noop = () => {};
+
+/**
+ * This hook registers our descendant by passing it into an array. We can then
+ * search that array by to find its index when registering it in the component.
+ * We use this for focus management, keyboard navigation, and typeahead
+ * functionality for some components.
+ *
+ * The hook accepts the element node
+ *
+ * Our main goals with this are:
+ * 1) maximum composability,
+ * 2) minimal API friction
+ * 3) SSR compatibility*
+ * 4) concurrent safe
+ * 5) index always up-to-date with the tree despite changes
+ * 6) works with memoization of any component in the tree (hopefully)
+ *
+ * * As for SSR, the good news is that we don't actually need the index on the
+ * server for most use-cases, as we are only using it to determine the order of
+ * composed descendants for keyboard navigation.
+ */
+export function useDescendant(descendant) {
+ const [, forceUpdate] = React.useState();
+ const {
+ registerDescendant = noop,
+ unregisterDescendant = noop,
+ descendants = [],
+ } = React.useContext(DescendantContext);
+
+ // This will initially return -1 because we haven't registered the descendant
+ // on the first render. After we register, this will then return the correct
+ // index on the following render and we will re-register descendants
+ // so that everything is up-to-date before the user interacts with a
+ // collection.
+ const index = findIndex(descendants, (item) => item.element === descendant.element);
+
+ const previousDescendants = usePrevious(descendants);
+
+ // We also need to re-register descendants any time ANY of the other
+ // descendants have changed. My brain was melting when I wrote this and it
+ // feels a little off, but checking in render and using the result in the
+ // effect's dependency array works well enough.
+ const someDescendantsHaveChanged = descendants.some((d, i) => {
+ return d.element !== previousDescendants?.[i]?.element;
+ });
+
+ // Prevent any flashing
+ useEnhancedEffect(() => {
+ if (!descendant.element) forceUpdate({});
+ registerDescendant({
+ ...descendant,
+ index,
+ });
+ return () => unregisterDescendant(descendant.element);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [
+ registerDescendant,
+ unregisterDescendant,
+ index,
+ someDescendantsHaveChanged,
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ ...Object.values(descendant),
+ ]);
+
+ return index;
+}
+
+export function useDescendantsInit() {
+ return React.useState([]);
+}
+
+export function useDescendants() {
+ return React.useContext(DescendantContext).descendants;
+}
+
+export function DescendantProvider(props) {
+ const { children, items, set } = props;
+
+ const registerDescendant = React.useCallback(
+ ({ element, index: explicitIndex, ...rest }) => {
+ if (!element) {
+ return;
+ }
+
+ set((oldItems) => {
+ let newItems;
+ if (explicitIndex != null) {
+ newItems = [
+ ...oldItems,
+ {
+ ...rest,
+ element,
+ index: explicitIndex,
+ },
+ ];
+ } else if (oldItems.length === 0) {
+ // If there are no items, register at index 0 and bail.
+ newItems = [
+ ...oldItems,
+ {
+ ...rest,
+ element,
+ index: 0,
+ },
+ ];
+ } else if (oldItems.find((item) => item.element === element)) {
+ // If the element is already registered, just use the same array
+ newItems = oldItems;
+ } else {
+ // When registering a descendant, we need to make sure we insert in
+ // into the array in the same order that it appears in the DOM. So as
+ // new descendants are added or maybe some are removed, we always know
+ // that the array is up-to-date and correct.
+ //
+ // So here we look at our registered descendants and see if the new
+ // element we are adding appears earlier than an existing descendant's
+ // DOM node via `node.compareDocumentPosition`. If it does, we insert
+ // the new element at this index. Because `registerDescendant` will be
+ // called in an effect every time the descendants state value changes,
+ // we should be sure that this index is accurate when descendent
+ // elements come or go from our component.
+ const index = findIndex(oldItems, (item) => {
+ if (!item.element || !element) {
+ return false;
+ }
+ // Does this element's DOM node appear before another item in the
+ // array in our DOM tree? If so, return true to grab the index at
+ // this point in the array so we know where to insert the new
+ // element.
+ return Boolean(
+ // eslint-disable-next-line no-bitwise
+ item.element.compareDocumentPosition(element) & Node.DOCUMENT_POSITION_PRECEDING,
+ );
+ });
+
+ const newItem = {
+ ...rest,
+ element,
+ index,
+ };
+
+ // If an index is not found we will push the element to the end.
+ if (index === -1) {
+ newItems = [...oldItems, newItem];
+ } else {
+ newItems = [...oldItems.slice(0, index), newItem, ...oldItems.slice(index)];
+ }
+ }
+ return newItems.map((item, index) => ({ ...item, index }));
+ });
+ },
+ // set is a state setter initialized by the useDescendants hook.
+ // We can safely ignore the lint warning here because it will not change
+ // between renders.
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ [],
+ );
+
+ const unregisterDescendant = React.useCallback(
+ (element) => {
+ if (!element) {
+ return;
+ }
+
+ set((oldItems) => oldItems.filter((item) => element !== item.element));
+ },
+ // set is a state setter initialized by the useDescendants hook.
+ // We can safely ignore the lint warning here because it will not change
+ // between renders.
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ [],
+ );
+
+ const value = React.useMemo(
+ () => ({
+ descendants: items,
+ registerDescendant,
+ unregisterDescendant,
+ }),
+ [items, registerDescendant, unregisterDescendant],
+ );
+
+ return {children};
+}
+
+DescendantProvider.propTypes = {
+ children: PropTypes.node,
+ items: PropTypes.array,
+ set: PropTypes.func,
+};
From 911e2e6adc6b9b3a6ded435e3701e3bea9dc36e7 Mon Sep 17 00:00:00 2001
From: joshwooding <12938082+joshwooding@users.noreply.github.com>
Date: Thu, 25 Jun 2020 17:15:37 +0100
Subject: [PATCH 2/8] Remove visibleNodes and use descendants.
---
.../material-ui-lab/src/TreeItem/TreeItem.js | 48 ++--
.../src/TreeItem/TreeItem.test.js | 13 +-
.../material-ui-lab/src/TreeView/TreeView.js | 266 +++++++++++-------
.../src/TreeView/descendants.js | 29 +-
4 files changed, 197 insertions(+), 159 deletions(-)
diff --git a/packages/material-ui-lab/src/TreeItem/TreeItem.js b/packages/material-ui-lab/src/TreeItem/TreeItem.js
index b81b39870931ee..6fe6750a78f872 100644
--- a/packages/material-ui-lab/src/TreeItem/TreeItem.js
+++ b/packages/material-ui-lab/src/TreeItem/TreeItem.js
@@ -7,7 +7,7 @@ import Collapse from '@material-ui/core/Collapse';
import { fade, withStyles, useTheme } from '@material-ui/core/styles';
import { useForkRef } from '@material-ui/core/utils';
import TreeViewContext from '../TreeView/TreeViewContext';
-import { useDescendant } from '../TreeView/descendants';
+import { DescendantProvider, useDescendant, useDescendantsInit } from '../TreeView/descendants';
export const styles = (theme) => ({
/* Styles applied to the root element. */
@@ -144,8 +144,9 @@ const TreeItem = React.forwardRef(function TreeItem(props, ref) {
const contentRef = React.useRef(null);
const handleRef = useForkRef(nodeRef, ref);
- const index = useDescendant({
+ const { index, parentId } = useDescendant({
element: nodeRef.current,
+ id: nodeId,
});
let icon = iconProp;
@@ -342,25 +343,20 @@ const TreeItem = React.forwardRef(function TreeItem(props, ref) {
};
React.useEffect(() => {
- if (registerNode) {
- const childIds = [];
- React.Children.forEach(children, (child) => {
- if (React.isValidElement(child) && child.props.nodeId) {
- childIds.push(child.props.nodeId);
- }
+ if (registerNode && unregisterNode && index !== -1) {
+ registerNode({
+ id: nodeId,
+ index,
+ parentId,
});
- registerNode(nodeId, childIds);
- }
- }, [children, nodeId, registerNode]);
- React.useEffect(() => {
- if (unregisterNode) {
return () => {
unregisterNode(nodeId);
};
}
+
return undefined;
- }, [nodeId, unregisterNode]);
+ }, [registerNode, unregisterNode, parentId, index, nodeId]);
React.useEffect(() => {
if (mapFirstChar && label) {
@@ -387,6 +383,8 @@ const TreeItem = React.forwardRef(function TreeItem(props, ref) {
ariaSelected = true;
}
+ const [descendants, setDescendants] = useDescendantsInit();
+
return (
{children && (
-
- {children}
-
+
+
+ {children}
+
+
)}
);
diff --git a/packages/material-ui-lab/src/TreeItem/TreeItem.test.js b/packages/material-ui-lab/src/TreeItem/TreeItem.test.js
index 0a67a33529abec..04b2a712c666cd 100644
--- a/packages/material-ui-lab/src/TreeItem/TreeItem.test.js
+++ b/packages/material-ui-lab/src/TreeItem/TreeItem.test.js
@@ -333,13 +333,10 @@ describe('', () => {
it('should work with programmatic focus', () => {
const { getByTestId } = render(
-
-
-
-
-
-
- ,
+
+
+
+ ,
);
expect(getByTestId('one')).to.have.attribute('tabindex', '0');
@@ -853,6 +850,7 @@ describe('', () => {
+
,
);
@@ -868,6 +866,7 @@ describe('', () => {
expect(getByTestId('three')).to.have.attribute('aria-expanded', 'true');
expect(getByTestId('five')).to.have.attribute('aria-expanded', 'true');
expect(getByTestId('six')).to.have.attribute('aria-expanded', 'false');
+ expect(getByTestId('eight')).not.to.have.attribute('aria-expanded');
});
});
});
diff --git a/packages/material-ui-lab/src/TreeView/TreeView.js b/packages/material-ui-lab/src/TreeView/TreeView.js
index 572a45acf70f55..99773af868b86d 100644
--- a/packages/material-ui-lab/src/TreeView/TreeView.js
+++ b/packages/material-ui-lab/src/TreeView/TreeView.js
@@ -15,14 +15,16 @@ export const styles = {
},
};
-function arrayDiff(arr1, arr2) {
- if (arr1.length !== arr2.length) return true;
-
- for (let i = 0; i < arr1.length; i += 1) {
- if (arr1[i] !== arr2[i]) return true;
+// To replace with Object.values() once we stop IE 11 support.
+function objectValues(obj) {
+ const res = [];
+ // eslint-disable-next-line no-restricted-syntax
+ for (const i in obj) {
+ if (obj.hasOwnProperty(i)) {
+ res.push(obj[i]);
+ }
}
-
- return false;
+ return res;
}
const findNextFirstChar = (firstChars, startIndex, char) => {
@@ -58,11 +60,11 @@ const TreeView = React.forwardRef(function TreeView(props, ref) {
} = props;
const [tabbable, setTabbable] = React.useState(null);
const [focusedNodeId, setFocusedNodeId] = React.useState(null);
+ const [descendants, setDescendants] = useDescendantsInit();
const nodeMap = React.useRef({});
const firstCharMap = React.useRef({});
- const visibleNodes = React.useRef([]);
const [expanded, setExpandedState] = useControlled({
controlled: expandedProp,
@@ -78,6 +80,12 @@ const TreeView = React.forwardRef(function TreeView(props, ref) {
state: 'selected',
});
+ const getChildren = (id) =>
+ objectValues(nodeMap.current)
+ .filter((node) => node.parentId === id)
+ .sort((a, b) => a.index - b.index)
+ .map((child) => child.id);
+
/*
* Status Helpers
*/
@@ -99,31 +107,126 @@ const TreeView = React.forwardRef(function TreeView(props, ref) {
*/
const getNextNode = (id) => {
- const nodeIndex = visibleNodes.current.indexOf(id);
- if (nodeIndex !== -1 && nodeIndex + 1 < visibleNodes.current.length) {
- return visibleNodes.current[nodeIndex + 1];
+ // If expanded get first child
+ if (isExpanded(id) && getChildren(id).length > 0) {
+ return getChildren(id)[0];
+ }
+
+ // Try to get next sibling
+ const node = nodeMap.current[id];
+ const siblings = getChildren(node.parentId);
+
+ const nextSibling = siblings[siblings.indexOf(id) + 1];
+
+ if (nextSibling) {
+ return nextSibling;
+ }
+
+ // try to get parent's next sibling
+ const parent = nodeMap.current[node.parentId];
+ if (parent) {
+ const parentSiblings = getChildren(parent.parentId);
+ return parentSiblings[parentSiblings.indexOf(parent.id) + 1];
}
return null;
};
const getPreviousNode = (id) => {
- const nodeIndex = visibleNodes.current.indexOf(id);
- if (nodeIndex !== -1 && nodeIndex - 1 >= 0) {
- return visibleNodes.current[nodeIndex - 1];
+ const node = nodeMap.current[id];
+ const siblings = getChildren(node.parentId);
+ const nodeIndex = siblings.indexOf(id);
+
+ if (nodeIndex === 0) {
+ return node.parentId;
}
- return null;
+
+ // start with previous node
+ let currentNode = siblings[nodeIndex - 1];
+ while (isExpanded(currentNode) && getChildren(currentNode).length > 0) {
+ currentNode = getChildren(currentNode).pop();
+ }
+
+ return currentNode;
+ };
+
+ const getLastNode = () => {
+ let lastNode = getChildren(null).pop();
+
+ while (isExpanded(lastNode)) {
+ lastNode = getChildren(lastNode).pop();
+ }
+ return lastNode;
};
+ const getFirstNode = () => getChildren(null)[0];
+ const getParent = (id) => nodeMap.current[id].parentId;
+
+ const getOrder = (nodeAId, nodeBId) => {
+ if (nodeAId === nodeBId) {
+ return [nodeAId, nodeBId];
+ }
+
+ const nodeA = nodeMap.current[nodeAId];
+ const nodeB = nodeMap.current[nodeBId];
+
+ if (nodeA.parentId === nodeB.id || nodeB.parentId === nodeA.id) {
+ return nodeB.parentId === nodeA.id ? [nodeA.id, nodeB.id] : [nodeB.id, nodeA.id];
+ }
+
+ const aFamily = [nodeA.id];
+ const bFamily = [nodeB.id];
+
+ let aAncestor = nodeA.parentId;
+ let bAncestor = nodeB.parentId;
+
+ let aAncestorIsCommon = bFamily.indexOf(aAncestor) !== -1;
+ let bAncestorIsCommon = aFamily.indexOf(bAncestor) !== -1;
+
+ let continueA = true;
+ let continueB = true;
+
+ while (!bAncestorIsCommon && !aAncestorIsCommon) {
+ if (continueA) {
+ aFamily.push(aAncestor);
+ aAncestorIsCommon = bFamily.indexOf(aAncestor) !== -1;
+ continueA = aAncestor !== null;
+ if (!aAncestorIsCommon && continueA) {
+ aAncestor = nodeMap.current[aAncestor].parentId;
+ }
+ }
+
+ if (continueB && !aAncestorIsCommon) {
+ bFamily.push(bAncestor);
+ bAncestorIsCommon = aFamily.indexOf(bAncestor) !== -1;
+ continueB = bAncestor !== null;
+ if (!bAncestorIsCommon && continueB) {
+ bAncestor = nodeMap.current[bAncestor].parentId;
+ }
+ }
+ }
+
+ const commonAncestor = aAncestorIsCommon ? aAncestor : bAncestor;
+ const ancestorFamily = getChildren(commonAncestor);
- const getLastNode = () => visibleNodes.current[visibleNodes.current.length - 1];
- const getFirstNode = () => visibleNodes.current[0];
- const getParent = (id) => nodeMap.current[id].parent;
+ const aSide = aFamily[aFamily.indexOf(commonAncestor) - 1];
+ const bSide = bFamily[bFamily.indexOf(commonAncestor) - 1];
+
+ return ancestorFamily.indexOf(aSide) < ancestorFamily.indexOf(bSide)
+ ? [nodeAId, nodeBId]
+ : [nodeBId, nodeAId];
+ };
const getNodesInRange = (a, b) => {
- const aIndex = visibleNodes.current.indexOf(a);
- const bIndex = visibleNodes.current.indexOf(b);
- const start = Math.min(aIndex, bIndex);
- const end = Math.max(aIndex, bIndex);
- return visibleNodes.current.slice(start, end + 1);
+ const [start, end] = getOrder(a, b);
+ const nodes = [start];
+
+ let current = start;
+
+ while (current !== end) {
+ current = getNextNode(current);
+ nodes.push(current);
+ }
+
+ return nodes;
};
/*
@@ -153,7 +256,7 @@ const TreeView = React.forwardRef(function TreeView(props, ref) {
Object.keys(firstCharMap.current).forEach((nodeId) => {
const firstChar = firstCharMap.current[nodeId];
const map = nodeMap.current[nodeId];
- const visible = map.parent ? isExpanded(map.parent) : true;
+ const visible = map.parentId ? isExpanded(map.parentId) : true;
if (visible) {
firstCharIds.push(nodeId);
@@ -187,11 +290,12 @@ const TreeView = React.forwardRef(function TreeView(props, ref) {
const toggleExpansion = (event, value = focusedNodeId) => {
let newExpanded;
+
if (expanded.indexOf(value) !== -1) {
newExpanded = expanded.filter((id) => id !== value);
setTabbable((oldTabbable) => {
const map = nodeMap.current[oldTabbable];
- if (oldTabbable && (map && map.parent ? map.parent.id : null) === value) {
+ if (oldTabbable && map && map.parentId === value) {
return value;
}
return oldTabbable;
@@ -209,15 +313,10 @@ const TreeView = React.forwardRef(function TreeView(props, ref) {
const expandAllSiblings = (event, id) => {
const map = nodeMap.current[id];
- const parent = nodeMap.current[map.parent];
+ const siblings = getChildren(map.parentId);
+
+ const diff = siblings.filter((child) => !isExpanded(child));
- let diff;
- if (parent) {
- diff = parent.children.filter((child) => !isExpanded(child));
- } else {
- const topLevelNodes = nodeMap.current[-1].children;
- diff = topLevelNodes.filter((node) => !isExpanded(node));
- }
const newExpanded = expanded.concat(diff);
if (diff.length > 0) {
@@ -395,25 +494,21 @@ const TreeView = React.forwardRef(function TreeView(props, ref) {
/*
* Mapping Helpers
*/
+ const registerNode = React.useCallback((node) => {
+ const { id, index, parentId } = node;
- const registerNode = (id, childrenIds) => {
- const currentMap = nodeMap.current[id];
- nodeMap.current[id] = { ...currentMap, children: childrenIds, id };
-
- childrenIds.forEach((childId) => {
- const currentChildMap = nodeMap.current[childId];
- nodeMap.current[childId] = { ...currentChildMap, parent: id, id: childId };
- });
- };
+ nodeMap.current[id] = { id, index, parentId };
+ }, []);
const getNodesToRemove = React.useCallback((id) => {
const map = nodeMap.current[id];
const nodes = [];
if (map) {
nodes.push(id);
- if (map.children) {
- nodes.concat(map.children);
- map.children.forEach((node) => {
+ const childNodes = getChildren(id);
+ if (childNodes.length > 0) {
+ nodes.concat(childNodes);
+ childNodes.forEach((node) => {
nodes.concat(getNodesToRemove(node));
});
}
@@ -424,98 +519,53 @@ const TreeView = React.forwardRef(function TreeView(props, ref) {
const cleanUpFirstCharMap = React.useCallback((nodes) => {
const newMap = { ...firstCharMap.current };
nodes.forEach((node) => {
- if (newMap[node]) {
- delete newMap[node];
- }
+ delete newMap[node];
});
firstCharMap.current = newMap;
}, []);
+ const cleanUpNodeMap = React.useCallback((nodes) => {
+ const newMap = { ...nodeMap.current };
+ nodes.forEach((node) => {
+ delete newMap[node];
+ });
+ nodeMap.current = newMap;
+ }, []);
+
const unregisterNode = React.useCallback(
(id) => {
const nodes = getNodesToRemove(id);
cleanUpFirstCharMap(nodes);
- const newMap = { ...nodeMap.current };
-
- nodes.forEach((node) => {
- const map = newMap[node];
- if (map) {
- if (map.parent) {
- const parentMap = newMap[map.parent];
- if (parentMap && parentMap.children) {
- const parentChildren = parentMap.children.filter((c) => c !== node);
- newMap[map.parent] = { ...parentMap, children: parentChildren };
- }
- }
-
- delete newMap[node];
- }
- });
- nodeMap.current = newMap;
+ cleanUpNodeMap(nodes);
setFocusedNodeId((oldFocusedNodeId) => {
- if (oldFocusedNodeId === id) {
+ if (nodeMap.current[oldFocusedNodeId] === undefined) {
return null;
}
return oldFocusedNodeId;
});
},
- [getNodesToRemove, cleanUpFirstCharMap],
+ [getNodesToRemove, cleanUpFirstCharMap, cleanUpNodeMap],
);
const mapFirstChar = (id, firstChar) => {
firstCharMap.current[id] = firstChar;
};
- const prevChildIds = React.useRef([]);
- const [childrenCalculated, setChildrenCalculated] = React.useState(false);
- React.useEffect(() => {
- const childIds = [];
-
- React.Children.forEach(children, (child) => {
- if (React.isValidElement(child) && child.props.nodeId) {
- childIds.push(child.props.nodeId);
- }
- });
- if (arrayDiff(prevChildIds.current, childIds)) {
- nodeMap.current[-1] = { parent: null, children: childIds };
-
- childIds.forEach((id, index) => {
- if (index === 0) {
- setTabbable(id);
- }
- });
- visibleNodes.current = nodeMap.current[-1].children;
- prevChildIds.current = childIds;
- setChildrenCalculated(true);
- }
- }, [children]);
-
React.useEffect(() => {
- const buildVisible = (nodes) => {
- let list = [];
- for (let i = 0; i < nodes.length; i += 1) {
- const item = nodes[i];
- list.push(item);
- const childs = nodeMap.current[item].children;
- if (isExpanded(item) && childs) {
- list = list.concat(buildVisible(childs));
- }
+ setTabbable((oldTabbable) => {
+ if (!oldTabbable) {
+ return descendants[0] ? descendants[0].id : null;
}
- return list;
- };
- if (childrenCalculated) {
- visibleNodes.current = buildVisible(nodeMap.current[-1].children);
- }
- }, [expanded, childrenCalculated, isExpanded, children]);
+ return oldTabbable;
+ });
+ }, [descendants]);
const noopSelection = () => {
return false;
};
- const [descendants, setDescendants] = useDescendantsInit();
-
return (
{
return d.element !== previousDescendants?.[i]?.element;
});
@@ -96,38 +96,25 @@ export function useDescendant(descendant) {
...Object.values(descendant),
]);
- return index;
+ return { parentId, index };
}
export function useDescendantsInit() {
return React.useState([]);
}
-export function useDescendants() {
- return React.useContext(DescendantContext).descendants;
-}
-
export function DescendantProvider(props) {
- const { children, items, set } = props;
+ const { children, items, set, id } = props;
const registerDescendant = React.useCallback(
- ({ element, index: explicitIndex, ...rest }) => {
+ ({ element, ...rest }) => {
if (!element) {
return;
}
set((oldItems) => {
let newItems;
- if (explicitIndex != null) {
- newItems = [
- ...oldItems,
- {
- ...rest,
- element,
- index: explicitIndex,
- },
- ];
- } else if (oldItems.length === 0) {
+ if (oldItems.length === 0) {
// If there are no items, register at index 0 and bail.
newItems = [
...oldItems,
@@ -210,8 +197,9 @@ export function DescendantProvider(props) {
descendants: items,
registerDescendant,
unregisterDescendant,
+ parentId: id,
}),
- [items, registerDescendant, unregisterDescendant],
+ [items, registerDescendant, unregisterDescendant, id],
);
return {children};
@@ -219,6 +207,7 @@ export function DescendantProvider(props) {
DescendantProvider.propTypes = {
children: PropTypes.node,
+ id: PropTypes.string,
items: PropTypes.array,
set: PropTypes.func,
};
From 3edb4e42cf09ac02fc747134e55cb6ca894e0bba Mon Sep 17 00:00:00 2001
From: joshwooding <12938082+joshwooding@users.noreply.github.com>
Date: Fri, 26 Jun 2020 00:08:32 +0100
Subject: [PATCH 3/8] Re-purpose bug test
---
.../material-ui-lab/src/TreeView/TreeView.test.js | 15 ++-------------
1 file changed, 2 insertions(+), 13 deletions(-)
diff --git a/packages/material-ui-lab/src/TreeView/TreeView.test.js b/packages/material-ui-lab/src/TreeView/TreeView.test.js
index 056a9fcbc9704d..7169af632ce66e 100644
--- a/packages/material-ui-lab/src/TreeView/TreeView.test.js
+++ b/packages/material-ui-lab/src/TreeView/TreeView.test.js
@@ -56,9 +56,7 @@ describe('', () => {
);
});
- // should not throw eventually or with a better error message
- // FIXME: https://github.com/mui-org/material-ui/issues/20832
- it('crashes when unmounting with duplicate ids', () => {
+ it('should not crash when unmounting with duplicate ids', () => {
const CustomTreeItem = () => {
return ;
};
@@ -89,16 +87,7 @@ describe('', () => {
expect(() => {
screen.getByRole('button').click();
- }).toErrorDev([
- 'RangeError: Maximum call stack size exceeded',
- 'The above error occurred in the component',
- ]);
-
- const {
- current: { errors },
- } = errorRef;
- expect(errors).to.have.length(1);
- expect(errors[0].toString()).to.include('RangeError: Maximum call stack size exceeded');
+ }).not.toErrorDev();
});
});
From 8f3bccd62ba7e6250267862218075fd6d6653702 Mon Sep 17 00:00:00 2001
From: joshwooding <12938082+joshwooding@users.noreply.github.com>
Date: Fri, 26 Jun 2020 01:19:35 +0100
Subject: [PATCH 4/8] Use #20147 and modify slightly
---
.../material-ui-lab/src/TreeItem/TreeItem.js | 4 ++-
.../src/TreeItem/TreeItem.test.js | 35 ++++++++++---------
.../material-ui-lab/src/TreeView/TreeView.js | 12 ++++---
.../src/TreeView/descendants.js | 4 +++
4 files changed, 33 insertions(+), 22 deletions(-)
diff --git a/packages/material-ui-lab/src/TreeItem/TreeItem.js b/packages/material-ui-lab/src/TreeItem/TreeItem.js
index 6fe6750a78f872..bffe020cc33dd1 100644
--- a/packages/material-ui-lab/src/TreeItem/TreeItem.js
+++ b/packages/material-ui-lab/src/TreeItem/TreeItem.js
@@ -343,11 +343,13 @@ const TreeItem = React.forwardRef(function TreeItem(props, ref) {
};
React.useEffect(() => {
+ // On the first render a node's index will be -1. We want to wait for the real index.
if (registerNode && unregisterNode && index !== -1) {
registerNode({
id: nodeId,
index,
parentId,
+ expandable,
});
return () => {
@@ -356,7 +358,7 @@ const TreeItem = React.forwardRef(function TreeItem(props, ref) {
}
return undefined;
- }, [registerNode, unregisterNode, parentId, index, nodeId]);
+ }, [registerNode, unregisterNode, parentId, index, nodeId, expandable]);
React.useEffect(() => {
if (mapFirstChar && label) {
diff --git a/packages/material-ui-lab/src/TreeItem/TreeItem.test.js b/packages/material-ui-lab/src/TreeItem/TreeItem.test.js
index 04b2a712c666cd..347c24231fb0f2 100644
--- a/packages/material-ui-lab/src/TreeItem/TreeItem.test.js
+++ b/packages/material-ui-lab/src/TreeItem/TreeItem.test.js
@@ -837,8 +837,9 @@ describe('', () => {
describe('asterisk key interaction', () => {
it('expands all siblings that are at the same level as the current node', () => {
+ const toggleSpy = spy();
const { getByTestId } = render(
-
+
@@ -862,6 +863,8 @@ describe('', () => {
fireEvent.keyDown(getByTestId('one'), { key: '*' });
+ expect(toggleSpy.args[0][1]).to.have.length(3);
+
expect(getByTestId('one')).to.have.attribute('aria-expanded', 'true');
expect(getByTestId('three')).to.have.attribute('aria-expanded', 'true');
expect(getByTestId('five')).to.have.attribute('aria-expanded', 'true');
@@ -989,27 +992,27 @@ describe('', () => {
fireEvent.keyDown(getByTestId('three'), { key: 'ArrowDown', shiftKey: true });
expect(getByTestId('four')).toHaveFocus();
- expect(container.querySelectorAll('[aria-selected=true]').length).to.equal(2);
+ expect(container.querySelectorAll('[aria-selected=true]')).to.have.length(2);
fireEvent.keyDown(getByTestId('four'), { key: 'ArrowDown', shiftKey: true });
expect(getByTestId('three')).to.have.attribute('aria-selected', 'true');
expect(getByTestId('four')).to.have.attribute('aria-selected', 'true');
expect(getByTestId('five')).to.have.attribute('aria-selected', 'true');
- expect(container.querySelectorAll('[aria-selected=true]').length).to.equal(3);
+ expect(container.querySelectorAll('[aria-selected=true]')).to.have.length(3);
fireEvent.keyDown(getByTestId('five'), { key: 'ArrowUp', shiftKey: true });
expect(getByTestId('four')).toHaveFocus();
- expect(container.querySelectorAll('[aria-selected=true]').length).to.equal(2);
+ expect(container.querySelectorAll('[aria-selected=true]')).to.have.length(2);
fireEvent.keyDown(getByTestId('four'), { key: 'ArrowUp', shiftKey: true });
- expect(container.querySelectorAll('[aria-selected=true]').length).to.equal(1);
+ expect(container.querySelectorAll('[aria-selected=true]')).to.have.length(1);
fireEvent.keyDown(getByTestId('three'), { key: 'ArrowUp', shiftKey: true });
- expect(container.querySelectorAll('[aria-selected=true]').length).to.equal(2);
+ expect(container.querySelectorAll('[aria-selected=true]')).to.have.length(2);
fireEvent.keyDown(getByTestId('two'), { key: 'ArrowUp', shiftKey: true });
@@ -1018,7 +1021,7 @@ describe('', () => {
expect(getByTestId('three')).to.have.attribute('aria-selected', 'true');
expect(getByTestId('four')).to.have.attribute('aria-selected', 'false');
expect(getByTestId('five')).to.have.attribute('aria-selected', 'false');
- expect(container.querySelectorAll('[aria-selected=true]').length).to.equal(3);
+ expect(container.querySelectorAll('[aria-selected=true]')).to.have.length(3);
});
specify('keyboard arrow does not select when selectionDisabled', () => {
@@ -1036,11 +1039,11 @@ describe('', () => {
fireEvent.keyDown(getByTestId('three'), { key: 'ArrowDown', shiftKey: true });
expect(getByTestId('four')).toHaveFocus();
- expect(container.querySelectorAll('[aria-selected=true]').length).to.equal(0);
+ expect(container.querySelectorAll('[aria-selected=true]')).to.have.length(0);
fireEvent.keyDown(getByTestId('four'), { key: 'ArrowUp', shiftKey: true });
- expect(container.querySelectorAll('[aria-selected=true]').length).to.equal(0);
+ expect(container.querySelectorAll('[aria-selected=true]')).to.have.length(0);
});
specify('keyboard arrow merge', () => {
@@ -1066,12 +1069,12 @@ describe('', () => {
fireEvent.keyDown(getByTestId('four'), { key: 'ArrowUp', shiftKey: true });
fireEvent.keyDown(getByTestId('three'), { key: 'ArrowUp', shiftKey: true });
- expect(container.querySelectorAll('[aria-selected=true]').length).to.equal(5);
+ expect(container.querySelectorAll('[aria-selected=true]')).to.have.length(5);
fireEvent.keyDown(getByTestId('two'), { key: 'ArrowDown', shiftKey: true });
fireEvent.keyDown(getByTestId('three'), { key: 'ArrowDown', shiftKey: true });
- expect(container.querySelectorAll('[aria-selected=true]').length).to.equal(3);
+ expect(container.querySelectorAll('[aria-selected=true]')).to.have.length(3);
});
specify('keyboard space', () => {
@@ -1178,11 +1181,11 @@ describe('', () => {
getByTestId('five').focus();
fireEvent.keyDown(getByTestId('five'), { key: 'End', shiftKey: true, ctrlKey: true });
- expect(container.querySelectorAll('[aria-selected=true]').length).to.equal(0);
+ expect(container.querySelectorAll('[aria-selected=true]')).to.have.length(0);
fireEvent.keyDown(getByTestId('nine'), { key: 'Home', shiftKey: true, ctrlKey: true });
- expect(container.querySelectorAll('[aria-selected=true]').length).to.equal(0);
+ expect(container.querySelectorAll('[aria-selected=true]')).to.have.length(0);
});
specify('mouse', () => {
@@ -1236,7 +1239,7 @@ describe('', () => {
fireEvent.click(getByText('five'));
fireEvent.click(getByText('nine'), { shiftKey: true });
- expect(container.querySelectorAll('[aria-selected=true]').length).to.equal(0);
+ expect(container.querySelectorAll('[aria-selected=true]')).to.have.length(0);
});
});
@@ -1317,7 +1320,7 @@ describe('', () => {
getByTestId('one').focus();
fireEvent.keyDown(getByTestId('one'), { key: 'a', ctrlKey: true });
- expect(container.querySelectorAll('[aria-selected=true]').length).to.equal(5);
+ expect(container.querySelectorAll('[aria-selected=true]')).to.have.length(5);
});
specify('ctrl + a does not select all when disableSelection', () => {
@@ -1334,7 +1337,7 @@ describe('', () => {
getByTestId('one').focus();
fireEvent.keyDown(getByTestId('one'), { key: 'a', ctrlKey: true });
- expect(container.querySelectorAll('[aria-selected=true]').length).to.equal(0);
+ expect(container.querySelectorAll('[aria-selected=true]')).to.have.length(0);
});
});
});
diff --git a/packages/material-ui-lab/src/TreeView/TreeView.js b/packages/material-ui-lab/src/TreeView/TreeView.js
index 99773af868b86d..beea03ab330f84 100644
--- a/packages/material-ui-lab/src/TreeView/TreeView.js
+++ b/packages/material-ui-lab/src/TreeView/TreeView.js
@@ -94,6 +94,8 @@ const TreeView = React.forwardRef(function TreeView(props, ref) {
[expanded],
);
+ const isExpandable = React.useCallback((id) => nodeMap.current[id]?.expandable, []);
+
const isSelected = React.useCallback(
(id) => (Array.isArray(selected) ? selected.indexOf(id) !== -1 : selected === id),
[selected],
@@ -295,7 +297,7 @@ const TreeView = React.forwardRef(function TreeView(props, ref) {
newExpanded = expanded.filter((id) => id !== value);
setTabbable((oldTabbable) => {
const map = nodeMap.current[oldTabbable];
- if (oldTabbable && map && map.parentId === value) {
+ if (oldTabbable && map?.parentId === value) {
return value;
}
return oldTabbable;
@@ -315,7 +317,7 @@ const TreeView = React.forwardRef(function TreeView(props, ref) {
const map = nodeMap.current[id];
const siblings = getChildren(map.parentId);
- const diff = siblings.filter((child) => !isExpanded(child));
+ const diff = siblings.filter((child) => isExpandable(child) && !isExpanded(child));
const newExpanded = expanded.concat(diff);
@@ -495,9 +497,9 @@ const TreeView = React.forwardRef(function TreeView(props, ref) {
* Mapping Helpers
*/
const registerNode = React.useCallback((node) => {
- const { id, index, parentId } = node;
+ const { id, index, parentId, expandable } = node;
- nodeMap.current[id] = { id, index, parentId };
+ nodeMap.current[id] = { id, index, parentId, expandable };
}, []);
const getNodesToRemove = React.useCallback((id) => {
@@ -555,7 +557,7 @@ const TreeView = React.forwardRef(function TreeView(props, ref) {
React.useEffect(() => {
setTabbable((oldTabbable) => {
if (!oldTabbable) {
- return descendants[0] ? descendants[0].id : null;
+ return descendants[0]?.id;
}
return oldTabbable;
diff --git a/packages/material-ui-lab/src/TreeView/descendants.js b/packages/material-ui-lab/src/TreeView/descendants.js
index 1b106335abeeb7..872cfb123c170a 100644
--- a/packages/material-ui-lab/src/TreeView/descendants.js
+++ b/packages/material-ui-lab/src/TreeView/descendants.js
@@ -1,6 +1,10 @@
import * as React from 'react';
import PropTypes from 'prop-types';
+/** Credit: https://github.com/reach/reach-ui/blob/86a046f54d53b6420e392b3fa56dd991d9d4e458/packages/descendants/README.md
+ * Modified slightly to suit our purposes.
+ */
+
// To replace with .findIndex() once we stop IE 11 support.
function findIndex(array, comp) {
for (let i = 0; i < array.length; i += 1) {
From 753fece96b494e425d0df14ee58eabe282f84d16 Mon Sep 17 00:00:00 2001
From: joshwooding <12938082+joshwooding@users.noreply.github.com>
Date: Sun, 28 Jun 2020 20:53:42 +0100
Subject: [PATCH 5/8] olivier review
---
.../src/TreeView/descendants.js | 27 +++++++++----------
1 file changed, 12 insertions(+), 15 deletions(-)
diff --git a/packages/material-ui-lab/src/TreeView/descendants.js b/packages/material-ui-lab/src/TreeView/descendants.js
index 872cfb123c170a..87bad1331ab1ea 100644
--- a/packages/material-ui-lab/src/TreeView/descendants.js
+++ b/packages/material-ui-lab/src/TreeView/descendants.js
@@ -77,19 +77,24 @@ export function useDescendant(descendant) {
// We also need to re-register descendants any time ANY of the other
// descendants have changed. My brain was melting when I wrote this and it
- // feels a little off, but checking in render and usin // effect's dependency array works well enough.g the result in the
- const someDescendantsHaveChanged = descendants.some((d, i) => {
- return d.element !== previousDescendants?.[i]?.element;
+ // feels a little off, but checking in render and using the result in the
+ // effect's dependency array works well enough.
+ const someDescendantsHaveChanged = descendants.some((newDescendant, position) => {
+ return newDescendant.element !== previousDescendants?.[position]?.element;
});
// Prevent any flashing
useEnhancedEffect(() => {
- if (!descendant.element) forceUpdate({});
+ if (!descendant.element) {
+ forceUpdate({});
+ }
registerDescendant({
...descendant,
index,
});
- return () => unregisterDescendant(descendant.element);
+ return () => {
+ unregisterDescendant(descendant.element);
+ };
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
registerDescendant,
@@ -174,11 +179,7 @@ export function DescendantProvider(props) {
return newItems.map((item, index) => ({ ...item, index }));
});
},
- // set is a state setter initialized by the useDescendants hook.
- // We can safely ignore the lint warning here because it will not change
- // between renders.
- // eslint-disable-next-line react-hooks/exhaustive-deps
- [],
+ [set],
);
const unregisterDescendant = React.useCallback(
@@ -189,11 +190,7 @@ export function DescendantProvider(props) {
set((oldItems) => oldItems.filter((item) => element !== item.element));
},
- // set is a state setter initialized by the useDescendants hook.
- // We can safely ignore the lint warning here because it will not change
- // between renders.
- // eslint-disable-next-line react-hooks/exhaustive-deps
- [],
+ [set],
);
const value = React.useMemo(
From fceb38dd186aa3369197132a3eb25b7e81420f5a Mon Sep 17 00:00:00 2001
From: joshwooding <12938082+joshwooding@users.noreply.github.com>
Date: Wed, 1 Jul 2020 23:18:03 +0100
Subject: [PATCH 6/8] sebastian's review
---
.../material-ui-lab/src/TreeView/TreeView.js | 71 ++++++++++---------
.../src/TreeView/descendants.js | 5 +-
2 files changed, 39 insertions(+), 37 deletions(-)
diff --git a/packages/material-ui-lab/src/TreeView/TreeView.js b/packages/material-ui-lab/src/TreeView/TreeView.js
index beea03ab330f84..59ca849255d2de 100644
--- a/packages/material-ui-lab/src/TreeView/TreeView.js
+++ b/packages/material-ui-lab/src/TreeView/TreeView.js
@@ -15,18 +15,6 @@ export const styles = {
},
};
-// To replace with Object.values() once we stop IE 11 support.
-function objectValues(obj) {
- const res = [];
- // eslint-disable-next-line no-restricted-syntax
- for (const i in obj) {
- if (obj.hasOwnProperty(i)) {
- res.push(obj[i]);
- }
- }
- return res;
-}
-
const findNextFirstChar = (firstChars, startIndex, char) => {
for (let i = startIndex; i < firstChars.length; i += 1) {
if (char === firstChars[i]) {
@@ -80,11 +68,18 @@ const TreeView = React.forwardRef(function TreeView(props, ref) {
state: 'selected',
});
- const getChildren = (id) =>
- objectValues(nodeMap.current)
- .filter((node) => node.parentId === id)
- .sort((a, b) => a.index - b.index)
- .map((child) => child.id);
+ const getChildrenIds = (id) => {
+ const childIds = [];
+
+ Object.keys(nodeMap.current).forEach((key) => {
+ const value = nodeMap.current[key];
+ if (value.parentId === id) {
+ childIds.splice(value.index, 0, value.id);
+ }
+ });
+
+ return childIds;
+ };
/*
* Status Helpers
@@ -110,13 +105,13 @@ const TreeView = React.forwardRef(function TreeView(props, ref) {
const getNextNode = (id) => {
// If expanded get first child
- if (isExpanded(id) && getChildren(id).length > 0) {
- return getChildren(id)[0];
+ if (isExpanded(id) && getChildrenIds(id).length > 0) {
+ return getChildrenIds(id)[0];
}
// Try to get next sibling
const node = nodeMap.current[id];
- const siblings = getChildren(node.parentId);
+ const siblings = getChildrenIds(node.parentId);
const nextSibling = siblings[siblings.indexOf(id) + 1];
@@ -127,7 +122,7 @@ const TreeView = React.forwardRef(function TreeView(props, ref) {
// try to get parent's next sibling
const parent = nodeMap.current[node.parentId];
if (parent) {
- const parentSiblings = getChildren(parent.parentId);
+ const parentSiblings = getChildrenIds(parent.parentId);
return parentSiblings[parentSiblings.indexOf(parent.id) + 1];
}
return null;
@@ -135,34 +130,44 @@ const TreeView = React.forwardRef(function TreeView(props, ref) {
const getPreviousNode = (id) => {
const node = nodeMap.current[id];
- const siblings = getChildren(node.parentId);
+ const siblings = getChildrenIds(node.parentId);
const nodeIndex = siblings.indexOf(id);
if (nodeIndex === 0) {
return node.parentId;
}
- // start with previous node
let currentNode = siblings[nodeIndex - 1];
- while (isExpanded(currentNode) && getChildren(currentNode).length > 0) {
- currentNode = getChildren(currentNode).pop();
+ while (isExpanded(currentNode) && getChildrenIds(currentNode).length > 0) {
+ currentNode = getChildrenIds(currentNode).pop();
}
return currentNode;
};
const getLastNode = () => {
- let lastNode = getChildren(null).pop();
+ let lastNode = getChildrenIds(null).pop();
while (isExpanded(lastNode)) {
- lastNode = getChildren(lastNode).pop();
+ lastNode = getChildrenIds(lastNode).pop();
}
return lastNode;
};
- const getFirstNode = () => getChildren(null)[0];
+ const getFirstNode = () => getChildrenIds(null)[0];
const getParent = (id) => nodeMap.current[id].parentId;
- const getOrder = (nodeAId, nodeBId) => {
+ /**
+ * This is used to determine the start and end of a selection range so
+ * we can get the nodes between the two border nodes.
+ *
+ * It finds the nodes' common ancestor using
+ * a naive implementation of a lowest common ancestor algorithm
+ * (https://en.wikipedia.org/wiki/Lowest_common_ancestor) and
+ * then compares the ancestors below it that are also above the
+ * given nodes so we can compare their indexes to work out which
+ * node is higher in the tree view.
+ */
+ const getOrderOfNodesInTree = (nodeAId, nodeBId) => {
if (nodeAId === nodeBId) {
return [nodeAId, nodeBId];
}
@@ -207,7 +212,7 @@ const TreeView = React.forwardRef(function TreeView(props, ref) {
}
const commonAncestor = aAncestorIsCommon ? aAncestor : bAncestor;
- const ancestorFamily = getChildren(commonAncestor);
+ const ancestorFamily = getChildrenIds(commonAncestor);
const aSide = aFamily[aFamily.indexOf(commonAncestor) - 1];
const bSide = bFamily[bFamily.indexOf(commonAncestor) - 1];
@@ -218,7 +223,7 @@ const TreeView = React.forwardRef(function TreeView(props, ref) {
};
const getNodesInRange = (a, b) => {
- const [start, end] = getOrder(a, b);
+ const [start, end] = getOrderOfNodesInTree(a, b);
const nodes = [start];
let current = start;
@@ -315,7 +320,7 @@ const TreeView = React.forwardRef(function TreeView(props, ref) {
const expandAllSiblings = (event, id) => {
const map = nodeMap.current[id];
- const siblings = getChildren(map.parentId);
+ const siblings = getChildrenIds(map.parentId);
const diff = siblings.filter((child) => isExpandable(child) && !isExpanded(child));
@@ -507,7 +512,7 @@ const TreeView = React.forwardRef(function TreeView(props, ref) {
const nodes = [];
if (map) {
nodes.push(id);
- const childNodes = getChildren(id);
+ const childNodes = getChildrenIds(id);
if (childNodes.length > 0) {
nodes.concat(childNodes);
childNodes.forEach((node) => {
diff --git a/packages/material-ui-lab/src/TreeView/descendants.js b/packages/material-ui-lab/src/TreeView/descendants.js
index 87bad1331ab1ea..88a98f91eb953e 100644
--- a/packages/material-ui-lab/src/TreeView/descendants.js
+++ b/packages/material-ui-lab/src/TreeView/descendants.js
@@ -16,10 +16,7 @@ function findIndex(array, comp) {
return -1;
}
-const useEnhancedEffect =
- typeof window !== 'undefined' && process.env.NODE_ENV !== 'test'
- ? React.useLayoutEffect
- : React.useEffect;
+const useEnhancedEffect = typeof window !== 'undefined' ? React.useLayoutEffect : React.useEffect;
const DescendantContext = React.createContext({});
From 3645d2049b0b76f931724c2b30af298660bd89e9 Mon Sep 17 00:00:00 2001
From: joshwooding <12938082+joshwooding@users.noreply.github.com>
Date: Thu, 2 Jul 2020 21:10:47 +0100
Subject: [PATCH 7/8] Re-word
---
.../material-ui-lab/src/TreeView/TreeView.js | 25 +++++++++++--------
1 file changed, 14 insertions(+), 11 deletions(-)
diff --git a/packages/material-ui-lab/src/TreeView/TreeView.js b/packages/material-ui-lab/src/TreeView/TreeView.js
index 59ca849255d2de..e54c4ca968ee8b 100644
--- a/packages/material-ui-lab/src/TreeView/TreeView.js
+++ b/packages/material-ui-lab/src/TreeView/TreeView.js
@@ -89,7 +89,7 @@ const TreeView = React.forwardRef(function TreeView(props, ref) {
[expanded],
);
- const isExpandable = React.useCallback((id) => nodeMap.current[id]?.expandable, []);
+ const isExpandable = React.useCallback((id) => nodeMap.current[id].expandable, []);
const isSelected = React.useCallback(
(id) => (Array.isArray(selected) ? selected.indexOf(id) !== -1 : selected === id),
@@ -162,12 +162,15 @@ const TreeView = React.forwardRef(function TreeView(props, ref) {
*
* It finds the nodes' common ancestor using
* a naive implementation of a lowest common ancestor algorithm
- * (https://en.wikipedia.org/wiki/Lowest_common_ancestor) and
- * then compares the ancestors below it that are also above the
- * given nodes so we can compare their indexes to work out which
- * node is higher in the tree view.
+ * (https://en.wikipedia.org/wiki/Lowest_common_ancestor).
+ * Then compares the ancestor's 2 children that are ancestors of nodeA and NodeB
+ * so we can compare their indexes to work out which node comes first in a depth first search.
+ * (https://en.wikipedia.org/wiki/Depth-first_search)
+ *
+ * Another way to put it is which node is shallower in a trémaux tree
+ * https://en.wikipedia.org/wiki/Tr%C3%A9maux_tree
*/
- const getOrderOfNodesInTree = (nodeAId, nodeBId) => {
+ const findOrderInTremauxTree = (nodeAId, nodeBId) => {
if (nodeAId === nodeBId) {
return [nodeAId, nodeBId];
}
@@ -222,13 +225,13 @@ const TreeView = React.forwardRef(function TreeView(props, ref) {
: [nodeBId, nodeAId];
};
- const getNodesInRange = (a, b) => {
- const [start, end] = getOrderOfNodesInTree(a, b);
- const nodes = [start];
+ const getNodesInRange = (nodeA, nodeB) => {
+ const [first, last] = findOrderInTremauxTree(nodeA, nodeB);
+ const nodes = [first];
- let current = start;
+ let current = first;
- while (current !== end) {
+ while (current !== last) {
current = getNextNode(current);
nodes.push(current);
}
From e120da932f7280529d00b203b640a52d549f3d97 Mon Sep 17 00:00:00 2001
From: joshwooding <12938082+joshwooding@users.noreply.github.com>
Date: Fri, 3 Jul 2020 01:38:53 +0100
Subject: [PATCH 8/8] More readable?
---
.../material-ui-lab/src/TreeView/TreeView.js | 24 ++++++++-----------
1 file changed, 10 insertions(+), 14 deletions(-)
diff --git a/packages/material-ui-lab/src/TreeView/TreeView.js b/packages/material-ui-lab/src/TreeView/TreeView.js
index e54c4ca968ee8b..c56271124fc8b0 100644
--- a/packages/material-ui-lab/src/TreeView/TreeView.js
+++ b/packages/material-ui-lab/src/TreeView/TreeView.js
@@ -68,19 +68,15 @@ const TreeView = React.forwardRef(function TreeView(props, ref) {
state: 'selected',
});
- const getChildrenIds = (id) => {
- const childIds = [];
-
- Object.keys(nodeMap.current).forEach((key) => {
- const value = nodeMap.current[key];
- if (value.parentId === id) {
- childIds.splice(value.index, 0, value.id);
- }
- });
-
- return childIds;
- };
-
+ // Using Object.keys -> .map to mimic Object.values we should replace with Object.values() once we stop IE 11 support.
+ const getChildrenIds = (id) =>
+ Object.keys(nodeMap.current)
+ .map((key) => {
+ return nodeMap.current[key];
+ })
+ .filter((node) => node.parentId === id)
+ .sort((a, b) => a.index - b.index)
+ .map((child) => child.id);
/*
* Status Helpers
*/
@@ -401,7 +397,7 @@ const TreeView = React.forwardRef(function TreeView(props, ref) {
};
const handleMultipleSelect = (event, value) => {
- let newSelected = [];
+ let newSelected;
if (selected.indexOf(value) !== -1) {
newSelected = selected.filter((id) => id !== value);
} else {