-
- }
- emptySelectedListText={
-
- Choose items of interest
-
- }
- initialStateNode={
-
- Start by searching for items
-
- }
- searchValue={pickerSearchValue}
- onChange={setPickerSearchValue}
- searchOnClear={() => setPickerSearchValue('')}
- includeNode={(node) => setSelectedNodes(_.concat([], selectedNodes, node))}
- removeNode={(node) => setSelectedNodes(_.reject(selectedNodes, { id: node.id }))}
- additionalClassNames={pickerSearchValue ? undefined : ['background-highlighted', 'test-class']}
- selectedTopSearch={
-
-
-
- }
- />
+ return res.data.posts.map((post) => ({
+ ...post,
+ label: post.title,
+ type: 'post',
+ }));
+ }}
+ />
+
{
+ const res = await axios.get('https://dummyjson.com/users?limit=5');
+ return res.data.users.map((entry) => ({
+ ...entry,
+ label: entry.firstName + ' ' + entry.lastName,
+ type: 'user',
+ }));
+ }}
+ />
+ >
+ ),
+ },
+ render: (args) => (
+
+
- );
-};
-
-export const Default: Story = {
- args: {},
- render: () => DefaultComponent(),
+ ),
};
diff --git a/src/components/TreePicker/TreePickerContext.jsx b/src/components/TreePicker/TreePickerContext.jsx
new file mode 100644
index 000000000..00968de0d
--- /dev/null
+++ b/src/components/TreePicker/TreePickerContext.jsx
@@ -0,0 +1,215 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { createStoreContext } from 'ryze';
+import _ from 'lodash';
+
+const { StoreProvider, useSlice, useStore } = createStoreContext();
+const RenderNodeCtx = React.createContext();
+export const HiddenByIdCtx = React.createContext({});
+
+export const useRenderNode = () => React.useContext(RenderNodeCtx);
+export const useHiddenById = () => React.useContext(HiddenByIdCtx);
+
+export const TreePickerProvider = ({ children, renderNode }) => {
+ return (
+
+ {children}
+
+ );
+};
+
+TreePickerProvider.propTypes = {
+ children: PropTypes.node.isRequired,
+ renderNode: PropTypes.func.isRequired,
+};
+
+export const useTreePickerSlice = useSlice;
+
+const getCurrentNode = (state) => state.paths[state.paths.length - 1];
+export const useTreePickerCurrentNode = () => useTreePickerSlice(getCurrentNode);
+export const useTreePickerPaths = () => useTreePickerSlice('paths');
+
+const getNodesForRendering = (state) => (!!state.search ? state.searchNodes : state.nodes);
+export const useTreePickerNodes = () => useTreePickerSlice(getNodesForRendering);
+
+export const useTreePickerSearch = () => useTreePickerSlice('search');
+
+export const useTreePickerGetState = () => {
+ const store = useStore();
+ return store.getState;
+};
+
+export const useInternalActions = () => {
+ const { setState } = useStore();
+ // breadcrumb navigate back to a node
+ const backTo = React.useCallback(
+ (nodeId) => {
+ setState((prev) => {
+ // nodeId: null -> go back to root
+ // nodeId: undefined -> go back 1 level
+ // nodeId: string -> go back to the node
+ const newPaths =
+ nodeId === null
+ ? []
+ : // if nodeId === undefined, index = -1, will end up slice without last item, if nodeId has a valid item, then index is positive
+ prev.paths.slice(
+ 0,
+ prev.paths.findIndex((entry) => entry.id === nodeId) + (nodeId === undefined ? 0 : 1)
+ );
+
+ const nodes = prev.treeMapRef.current.get(nodeId || newPaths[newPaths.length - 1]?.id || '');
+
+ if (!nodes) {
+ console.error('[TreePicker] Unexpected node id: ', nodeId);
+ return prev;
+ }
+
+ return {
+ ...prev,
+ paths: newPaths,
+ nodes,
+ search: '',
+ };
+ });
+ },
+ [setState]
+ );
+
+ // expand new node & init root
+ const goTo = React.useCallback(
+ (nodes, node) => {
+ setState((prev) => {
+ if (!node) {
+ // init root
+ prev.treeMapRef.current.set('', nodes);
+
+ return {
+ ...prev,
+ nodes: nodes,
+ search: '',
+ };
+ }
+
+ const currentNode = prev.paths[prev.paths.length - 1];
+ // when node is the same as currentNode, do no change paths
+ const newPaths = currentNode && currentNode.id === node.id ? prev.paths : [...prev.paths, node];
+
+ prev.treeMapRef.current.set(node.id, nodes);
+
+ return {
+ ...prev,
+ paths: newPaths,
+ nodes: nodes,
+ search: '',
+ };
+ });
+ },
+ [setState]
+ );
+
+ const searchTo = React.useCallback(
+ (nodes, search) => {
+ setState((prev) => {
+ if (search) {
+ return {
+ ...prev,
+ search: search,
+ searchNodes: nodes,
+ };
+ }
+
+ // clear search
+ // const node = prev.paths[prev.paths.length - 1];
+ // if node is falsy, assume it is searching in root
+ // const newNodes = prev.treeMapRef.current.get(node ? node.id : '');
+ return {
+ ...prev,
+ search: '',
+ searchNodes: [],
+ };
+ });
+ },
+ [setState]
+ );
+
+ const addAddable = React.useCallback(
+ (nodeId, addNode) => {
+ setState((prev) => {
+ if (!nodeId || !addNode) return prev;
+
+ return prev.addable[nodeId]
+ ? prev
+ : {
+ ...prev,
+ addable: { ...prev.addable, [nodeId]: addNode },
+ };
+ });
+ },
+ [setState]
+ );
+
+ const removeAddable = React.useCallback(
+ (nodeId) => {
+ setState((prev) => {
+ if (!nodeId) return prev;
+
+ return !prev.addable[nodeId]
+ ? prev
+ : {
+ ...prev,
+ addable: _.pickBy(prev.addable, (_value, id) => id !== nodeId),
+ };
+ });
+ },
+ [setState]
+ );
+
+ const setIsResolvingRoot = React.useCallback(
+ (newValue) => {
+ setState((prev) => ({
+ ...prev,
+ isResolvingRoot: newValue,
+ }));
+ },
+ [setState]
+ );
+
+ const setExpandingNodeId = React.useCallback(
+ (nodeId, expanding) => {
+ setState((prev) => ({
+ ...prev,
+ expandingNodeId: expanding ? nodeId : prev.expandingNodeId === nodeId ? '' : prev.expandingNodeId,
+ }));
+ },
+ [setState]
+ );
+
+ return {
+ backTo,
+ goTo,
+ searchTo,
+ addAddable,
+ removeAddable,
+ setIsResolvingRoot,
+ setExpandingNodeId,
+ };
+};
+
+export const useTreePickerActions = () => {
+ const { backTo, goTo, searchTo } = useInternalActions();
+ return { backTo, goTo, searchTo };
+};
diff --git a/src/components/TreePicker/TreePickerHeader.css b/src/components/TreePicker/TreePickerHeader.css
new file mode 100644
index 000000000..7829a8240
--- /dev/null
+++ b/src/components/TreePicker/TreePickerHeader.css
@@ -0,0 +1,18 @@
+@import url('../../styles/variable.css');
+
+.aui--tree-picker-header {
+ color: $color-text-description;
+}
+
+.aui--tree-picker-header-action-holder {
+ /* 24 + 12 + 24 */
+ width: 60px;
+}
+
+.aui--tree-picker-header-include-all.aui--button.aui-link {
+ width: 60px;
+
+ & .aui-children-container {
+ font-weight: $font-weight-bold;
+ }
+}
diff --git a/src/components/TreePicker/TreePickerHeader.jsx b/src/components/TreePicker/TreePickerHeader.jsx
new file mode 100644
index 000000000..2c53b9f00
--- /dev/null
+++ b/src/components/TreePicker/TreePickerHeader.jsx
@@ -0,0 +1,58 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import cc from 'classnames';
+import _ from 'lodash';
+import Button from '../Button';
+import { useTreePickerGetState, useTreePickerSlice } from './TreePickerContext';
+
+import './TreePickerHeader.css';
+
+const getNoAddable = (state) => _.isEmpty(state.addable);
+
+const HeaderIncludeAll = () => {
+ const noAddable = useTreePickerSlice(getNoAddable);
+ const getTreeState = useTreePickerGetState();
+
+ return noAddable ? (
+
+ ) : (
+
+ );
+};
+
+const getCurrentNodeHeader = (state) => {
+ const currentNode = state.paths[state.paths.length - 1];
+ return currentNode?.header;
+};
+
+const TreePickerHeader = ({ children, className }) => {
+ const header = useTreePickerSlice(getCurrentNodeHeader);
+
+ const content = header || children;
+
+ return !content ? null : (
+
+ );
+};
+
+TreePickerHeader.propTypes = {
+ children: PropTypes.node,
+ className: PropTypes.string,
+};
+
+export default TreePickerHeader;
diff --git a/src/components/TreePicker/TreePickerNav.css b/src/components/TreePicker/TreePickerNav.css
new file mode 100644
index 000000000..d9a283012
--- /dev/null
+++ b/src/components/TreePicker/TreePickerNav.css
@@ -0,0 +1,3 @@
+.aui--tree-picker-nav {
+ margin-left: 2px;
+}
diff --git a/src/components/TreePicker/TreePickerNav.jsx b/src/components/TreePicker/TreePickerNav.jsx
new file mode 100644
index 000000000..9cb182f97
--- /dev/null
+++ b/src/components/TreePicker/TreePickerNav.jsx
@@ -0,0 +1,36 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import cx from 'classnames';
+import Breadcrumb from '../Breadcrumb';
+import { useInternalActions, useTreePickerPaths } from './TreePickerContext';
+
+import './TreePickerNav.css';
+
+const TreePickerNav = ({ rootLabel = 'All', className, onNavTo }) => {
+ const paths = useTreePickerPaths();
+ const { backTo } = useInternalActions();
+
+ const rootNode = { id: '__all__', label: rootLabel };
+
+ return paths.length === 0 ? null : (
+ {
+ const newPath = pathId === rootNode.id ? null : pathId;
+ backTo(newPath);
+ onNavTo?.(newPath);
+ }}
+ />
+ );
+};
+
+TreePickerNav.propTypes = {
+ rootLabel: PropTypes.string,
+ className: PropTypes.string,
+ onNavTo: PropTypes.func,
+};
+
+export default TreePickerNav;
diff --git a/src/components/TreePicker/TreePickerNode.css b/src/components/TreePicker/TreePickerNode.css
new file mode 100644
index 000000000..32d89038a
--- /dev/null
+++ b/src/components/TreePicker/TreePickerNode.css
@@ -0,0 +1,32 @@
+@import url('../../styles/variable.css');
+
+.aui--tree-picker-node-expand {
+ margin: 0;
+
+ &.aui--button.aui-borderless:hover {
+ border-color: transparent;
+ }
+}
+
+.aui--tree-picker-node-add {
+ margin: 0;
+}
+
+.aui--tree-picker-node-svg {
+ width: 16px;
+ height: 16px;
+}
+
+.aui--tree-picker-node-branch {
+ margin-left: 24px;
+ border-top: 1px solid $color-border-base;
+ border-left: 1px solid $color-border-base;
+
+ & .aui--tree-picker-row {
+ padding-left: 12px;
+
+ &:last-child {
+ border-bottom: 0;
+ }
+ }
+}
diff --git a/src/components/TreePicker/TreePickerNode.jsx b/src/components/TreePicker/TreePickerNode.jsx
new file mode 100644
index 000000000..c0337e660
--- /dev/null
+++ b/src/components/TreePicker/TreePickerNode.jsx
@@ -0,0 +1,280 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import cx from 'classnames';
+import _ from 'lodash';
+import useIsUnmounted from '../../hooks/useIsUnmounted';
+import useCallbackRef from '../../hooks/useCallbackRef';
+import PlusIcon from '../../styles/icons/plus.svg';
+import FolderOpenIcon from '../../styles/icons/folder-open.svg';
+import FolderIcon from '../../styles/icons/folder.svg';
+import Button from '../Button';
+import Skeleton from '../Skeleton';
+import {
+ useTreePickerGetState,
+ useRenderNode,
+ useInternalActions,
+ useHiddenById,
+ useTreePickerSlice,
+} from './TreePickerContext';
+
+import './TreePickerNode.css';
+
+const TreePickerNodeContext = React.createContext({ level: 0 });
+
+export const useTreePickerNode = () => React.useContext(TreePickerNodeContext);
+
+const TreePickerNode = ({ className, children, node }) => {
+ const { goTo } = useInternalActions();
+ const [state, setState] = React.useState(() => ({ expanded: false, subNodes: [] }));
+
+ const { level } = useTreePickerNode();
+
+ const inlineExpand = React.useCallback(
+ (nodes) => {
+ setState((prev) => {
+ if (!prev.expanded && nodes) {
+ return { expanded: true, subNodes: nodes };
+ }
+ return { expanded: false, subNodes: [] };
+ });
+ },
+ [setState]
+ );
+
+ const fullExpand = React.useCallback(
+ (nodes) => {
+ goTo(nodes, node);
+ },
+ [goTo, node]
+ );
+
+ const value = React.useMemo(
+ () => ({
+ level: level + 1,
+ node,
+ expanded: state.expanded,
+ inlineExpand,
+ fullExpand,
+ }),
+ [level, node, state.expanded, inlineExpand, fullExpand]
+ );
+
+ return (
+
+ {children}
+ {state.subNodes.length === 0 ? null : }
+
+ );
+};
+TreePickerNode.propTypes = {
+ className: PropTypes.string,
+ children: PropTypes.node.isRequired,
+ node: PropTypes.shape({
+ id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
+ label: PropTypes.string.isRequired,
+ }).isRequired,
+};
+
+const TreePickerNodeBranch = ({ nodes }) => {
+ const renderNode = useRenderNode();
+ const { level } = useTreePickerNode();
+ const byId = useHiddenById();
+
+ const visibleNodes = nodes.filter(({ id }) => !byId[id]);
+
+ return visibleNodes.length === 0 ? null : (
+
+ {visibleNodes.map(renderNode)}
+
+ );
+};
+
+const TreePickerNodeContent = ({ children, className, ...rest }) => {
+ return (
+
+ {children}
+
+ );
+};
+TreePickerNodeContent.propTypes = {
+ children: PropTypes.node.isRequired,
+ className: PropTypes.string,
+};
+
+const getReverseIcon = (current) =>
+ ({
+ 'folder-open': 'folder',
+ folder: 'folder-open',
+ }[current]);
+
+const IconComponents = {
+ 'folder-open': FolderOpenIcon,
+ folder: FolderIcon,
+};
+
+const getHasSearch = (state) => !!state.search;
+
+const TreePickerNodeExpand = ({
+ className,
+ inline,
+ onClick,
+ disabled,
+ onMouseEnter,
+ onMouseLeave,
+ resolveNodes,
+ ...rest
+}) => {
+ const [isHover, setIsHover] = React.useState(false);
+ const [isLoading, setIsLoading] = React.useState(false);
+ const { setExpandingNodeId } = useInternalActions();
+ const expandableRef = useTreePickerSlice('expandableRef');
+ const { node, expanded, inlineExpand, fullExpand } = useTreePickerNode();
+ const isUnmounted = useIsUnmounted();
+ const getTreeState = useTreePickerGetState();
+ const hasSearch = useTreePickerSlice(getHasSearch);
+
+ const iconName = expanded ? 'folder-open' : 'folder';
+ const ExpandIcon = IconComponents[isHover ? getReverseIcon(iconName) : iconName];
+
+ const propExpand = inline ? inlineExpand : fullExpand;
+ const defaultExpand = hasSearch ? inlineExpand : fullExpand;
+ const shouldDefaultExpand = _.isNil(inline);
+
+ const handleExpand = async () => {
+ setExpandingNodeId(node.id, true);
+ const expandFunction = shouldDefaultExpand ? defaultExpand : propExpand;
+
+ setIsLoading(true);
+ try {
+ const nodes = await resolveNodes();
+
+ // ignore falsy nodes
+ // or when user click 2 different expands, ignore the first click
+ // or if node is unmounted
+ if (!nodes || getTreeState().expandingNodeId !== node.id || isUnmounted()) return;
+
+ expandFunction(nodes);
+ } catch (error) {
+ console.error('[TreePickerNodeExpand]', error);
+ } finally {
+ setIsLoading(false);
+ setExpandingNodeId(node.id, false);
+ }
+ };
+
+ // record the current expand node to ctx ref
+ React.useLayoutEffect(() => {
+ if (!disabled) {
+ expandableRef.current[node.id] = handleExpand;
+ return;
+ }
+ delete expandableRef.current[node.id];
+ });
+
+ // when using default expand, on search reset it should go back to collapsed
+ React.useEffect(() => {
+ if (!hasSearch && shouldDefaultExpand) {
+ inlineExpand();
+ }
+ }, [hasSearch, shouldDefaultExpand, inlineExpand]);
+
+ return (
+ }
+ {...rest}
+ disabled={disabled}
+ isLoading={isLoading}
+ onClick={async (event) => {
+ if (!resolveNodes) return onClick?.(event);
+ await handleExpand();
+ onClick?.(event);
+ }}
+ className={cx('aui--tree-picker-node-expand', className)}
+ onMouseEnter={(event) => {
+ setIsHover(true);
+ return onMouseEnter?.(event);
+ }}
+ onMouseLeave={(event) => {
+ setIsHover(false);
+ return onMouseLeave?.(event);
+ }}
+ />
+ );
+};
+TreePickerNodeExpand.propTypes = {
+ inline: PropTypes.bool,
+ resolveNodes: PropTypes.func,
+ className: PropTypes.string,
+ onClick: PropTypes.func,
+ disabled: PropTypes.bool,
+ onMouseEnter: PropTypes.func,
+ onMouseLeave: PropTypes.func,
+};
+
+const TreePickerNodeAdd = ({ className, onAdd, onClick, disabled = false, ...rest }) => {
+ const { node } = useTreePickerNode();
+ const { addAddable, removeAddable } = useInternalActions();
+ const addNode = (isIncludeAll = false) => {
+ onAdd(node, isIncludeAll);
+ };
+ const addNodeRef = useCallbackRef(addNode);
+
+ // update addable based on disabled
+ React.useEffect(() => {
+ if (!disabled) {
+ addAddable(node.id, addNodeRef);
+ } else {
+ removeAddable(node.id);
+ }
+ }, [node.id, addAddable, removeAddable, disabled, addNodeRef]);
+
+ // remove addable when node unmounts
+ React.useEffect(() => {
+ return () => {
+ removeAddable(node.id);
+ };
+ }, [node.id, removeAddable, addNodeRef]);
+
+ return (
+ }
+ color="primary"
+ variant="inverse"
+ {...rest}
+ disabled={disabled}
+ onClick={(event) => {
+ addNode();
+ return onClick?.(event);
+ }}
+ className={cx('aui--tree-picker-node-add', className)}
+ />
+ );
+};
+TreePickerNodeAdd.propTypes = {
+ onAdd: PropTypes.func.isRequired,
+ onClick: PropTypes.func,
+ disabled: PropTypes.bool,
+ className: PropTypes.string,
+};
+
+const TreePickerNodePlaceholder = () => {
+ return (
+
+
+
+
+
+
+
+ );
+};
+
+TreePickerNode.Content = TreePickerNodeContent;
+TreePickerNode.Expand = TreePickerNodeExpand;
+TreePickerNode.Add = TreePickerNodeAdd;
+TreePickerNode.Placeholder = TreePickerNodePlaceholder;
+
+export default TreePickerNode;
diff --git a/src/components/TreePicker/TreePickerSearch.css b/src/components/TreePicker/TreePickerSearch.css
new file mode 100644
index 000000000..b8bfe1202
--- /dev/null
+++ b/src/components/TreePicker/TreePickerSearch.css
@@ -0,0 +1,7 @@
+@import url('../../styles/variable.css');
+
+.aui--tree-picker-search {
+ & .aui--search-component-icon {
+ padding: 8px;
+ }
+}
diff --git a/src/components/TreePicker/TreePickerSearch.jsx b/src/components/TreePicker/TreePickerSearch.jsx
new file mode 100644
index 000000000..0cccbc556
--- /dev/null
+++ b/src/components/TreePicker/TreePickerSearch.jsx
@@ -0,0 +1,77 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import cx from 'classnames';
+import useIsUnmounted from '../../hooks/useIsUnmounted';
+import Search from '../Search';
+import {
+ useTreePickerGetState,
+ useInternalActions,
+ useTreePickerSearch,
+ useTreePickerSlice,
+} from './TreePickerContext';
+
+import './TreePickerSearch.css';
+
+const getIsInternalDisabled = (state) => state.isResolvingRoot || !!state.expandingNodeId;
+
+const TreePickerSearch = ({ resolveNodes, className, disabled, ...rest }) => {
+ const [isLoading, setIsLoading] = React.useState();
+
+ const inputRef = React.useRef();
+ const textRef = React.useRef('');
+ const isUnmounted = useIsUnmounted();
+ const search = useTreePickerSearch();
+ const { searchTo } = useInternalActions();
+ const internalDisable = useTreePickerSlice(getIsInternalDisabled);
+ const getTreeState = useTreePickerGetState();
+
+ // TODO fix Search
+ React.useLayoutEffect(() => {
+ inputRef.current.value = search;
+ }, [search]);
+
+ const isDisabled = disabled ?? internalDisable;
+
+ return (
+ {
+ if (!searchText) return searchTo();
+
+ textRef.current = searchText;
+ setIsLoading(true);
+ try {
+ const paths = getTreeState().paths;
+ const currentNode = paths[paths.length - 1] || null;
+ const nodes = await resolveNodes(searchText, currentNode);
+
+ // ignore falsy nodes
+ // or if search itself is unmounted
+ // or when user types in quick succession
+ // ignore the old search results
+ if (!nodes || textRef.current !== searchText || isUnmounted()) return;
+
+ searchTo(nodes, searchText);
+ } catch (error) {
+ console.error('[TreePickerSearch]', error);
+ } finally {
+ setIsLoading(false);
+ }
+ }}
+ onClear={() => searchTo()}
+ />
+ );
+};
+
+TreePickerSearch.propTypes = {
+ resolveNodes: PropTypes.func.isRequired,
+ className: PropTypes.string,
+ disabled: PropTypes.bool,
+};
+
+export default TreePickerSearch;
diff --git a/src/components/TreePicker/TreePickerTree.css b/src/components/TreePicker/TreePickerTree.css
new file mode 100644
index 000000000..e23173996
--- /dev/null
+++ b/src/components/TreePicker/TreePickerTree.css
@@ -0,0 +1,27 @@
+@import url('../../styles/variable.css');
+
+.aui--tree-picker-tree {
+ display: flex;
+ flex-direction: column;
+ border-top: 1px solid $color-border-base;
+ border-bottom: 1px solid $color-border-base;
+
+ & .aui--tree-picker-row:first-child {
+ border-top: 0;
+ }
+}
+
+.aui--tree-picker-empty {
+ background-color: $color-grey-100;
+ min-height: 182px;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ gap: 12px;
+
+ & .empty-title {
+ font-size: $font-size-large;
+ font-weight: $font-weight-bold;
+ }
+}
diff --git a/src/components/TreePicker/TreePickerTree.jsx b/src/components/TreePicker/TreePickerTree.jsx
new file mode 100644
index 000000000..cfdd7cfad
--- /dev/null
+++ b/src/components/TreePicker/TreePickerTree.jsx
@@ -0,0 +1,136 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import cx from 'classnames';
+import _ from 'lodash';
+import useIsUnmounted from '../../hooks/useIsUnmounted';
+import Button from '../Button';
+import {
+ useTreePickerSlice,
+ useTreePickerNodes,
+ useTreePickerSearch,
+ useInternalActions,
+ useRenderNode,
+ HiddenByIdCtx,
+} from './TreePickerContext';
+import TreePickerNode from './TreePickerNode';
+
+import './TreePickerTree.css';
+
+const TreePickerTreePlaceholder = ({ count = 5 }) => {
+ return Array(count)
+ .fill(0)
+ .map((_value, index) => );
+};
+TreePickerTreePlaceholder.propTypes = {
+ count: PropTypes.number,
+};
+
+const getIsInRoot = (state) => state.paths.length === 0;
+
+const TreePickerTreeEmptyState = ({ title = 'No Results.' }) => {
+ const isInRoot = useTreePickerSlice(getIsInRoot);
+ const search = useTreePickerSearch();
+ const { backTo, searchTo } = useInternalActions();
+
+ const backAction = isInRoot ? null : (
+
+ );
+ return (
+
+
{title}
+ {search ? (
+
+ ) : (
+ backAction
+ )}
+
+ );
+};
+TreePickerTreeEmptyState.propTypes = {
+ title: PropTypes.node,
+};
+
+const defaultPlaceholder = ;
+const defaultEmptyState = ;
+
+const TreePickerTree = ({
+ className,
+ resolveRootNodes,
+ hiddenNodeIds,
+ placeholder = defaultPlaceholder,
+ emptyState = defaultEmptyState,
+}) => {
+ const isInRoot = useTreePickerSlice(getIsInRoot);
+ const nodes = useTreePickerNodes();
+ const renderNode = useRenderNode();
+ const { goTo, setIsResolvingRoot } = useInternalActions();
+ const isResolvingRoot = useTreePickerSlice('isResolvingRoot');
+ const isUnmounted = useIsUnmounted();
+
+ React.useEffect(() => {
+ let cancelled = false;
+ const resolve = async () => {
+ if (!isInRoot) return;
+
+ setIsResolvingRoot(true);
+ try {
+ const rootNodes = await resolveRootNodes();
+
+ // ignore falsy nodes
+ // or if the tree itself is unmounted
+ // or if the resolveRootNodes func has changed
+ if (!rootNodes || isUnmounted() || cancelled) return;
+
+ goTo(rootNodes);
+ } catch (error) {
+ console.error('[TreePickerTree]', error);
+ } finally {
+ if (cancelled) return;
+ setIsResolvingRoot(false);
+ }
+ };
+ resolve();
+
+ return () => {
+ cancelled = true;
+ };
+ }, [goTo, setIsResolvingRoot, isUnmounted, resolveRootNodes, isInRoot]);
+
+ const byId = React.useMemo(() => _.keyBy(hiddenNodeIds), [hiddenNodeIds]);
+ const visibleNodes = nodes.filter(({ id }) => !byId[id]);
+
+ return (
+
+
+ {isResolvingRoot ? placeholder : visibleNodes.length === 0 ? emptyState : visibleNodes.map(renderNode)}
+
+
+ );
+};
+
+TreePickerTree.propTypes = {
+ className: PropTypes.string,
+ hiddenNodeIds: PropTypes.array,
+ resolveRootNodes: PropTypes.func.isRequired,
+ placeholder: PropTypes.node,
+ emptyState: PropTypes.node,
+};
+
+TreePickerTree.Placeholder = TreePickerTreePlaceholder;
+TreePickerTree.EmptyState = TreePickerTreeEmptyState;
+
+export default TreePickerTree;
diff --git a/src/components/TreePicker/index.d.ts b/src/components/TreePicker/index.d.ts
index bd2f4b623..ce9699a2c 100644
--- a/src/components/TreePicker/index.d.ts
+++ b/src/components/TreePicker/index.d.ts
@@ -1,167 +1,98 @@
import * as React from 'react';
-export interface TreePickerSimplePureBreadcrumbNodes {
- id?: any;
- label: string;
+export interface TreePickerNodeContentProps {
+ className?: string;
+ children: React.ReactNode;
}
-export interface TreePickerSimplePureSelectedNodes {
- id?: any;
- label: string;
- isExpandable?: boolean;
- path?: {
- id?: any;
- label: string;
- }[];
- ancestors?: {
- id?: any;
- label: string;
- }[];
- type: string;
- value?: number;
- accent?: 'warning' | 'success' | 'info' | 'error';
+declare const TreePickerNodeContent: React.FC;
+
+export interface TreePickerNodeExpandProps {
+ className?: string;
+ inline?: boolean;
+ resolveNodes?: (...args: any[]) => any;
+ onClick?: (...args: any[]) => any;
+ disabled?: boolean;
+ onMouseEnter?: (...args: any[]) => any;
+ onMouseLeave?: (...args: any[]) => any;
}
-export interface TreePickerSimplePureSubtree {
- id?: any;
- label: string;
- isExpandable?: boolean;
- path?: {
- id?: any;
- label: string;
- }[];
- ancestors?: {
- id?: any;
- label: string;
- }[];
- type: string;
- value?: number;
- accent?: 'warning' | 'success' | 'info' | 'error';
+declare const TreePickerNodeExpand: React.FC;
+
+export interface TreePickerNodeAddProps {
+ onAdd: (...args: any[]) => any;
+ onClick?: (...args: any[]) => any;
+ disabled?: boolean;
+ className?: string;
}
-export interface TreePickerSimplePureBreadcrumbRootNode {
- id?: any;
+declare const TreePickerNodeAdd: React.FC;
+
+declare const TreePickerNodePlaceholder: React.FC;
+
+export interface TreePickerNodeNode {
+ id: string | number;
label: string;
}
-export interface TreePickerSimplePureProps {
- /**
- * Class Names for SplitPane component
- */
- additionalClassNames?: string[];
- /**
- * Returns node id. This prop is not required, but an empty array is not allowed. At least one element is required in the array.
- */
- breadcrumbNodes?: TreePickerSimplePureBreadcrumbNodes[];
- /**
- * This propType creates a list of breadcrumb node
- */
- breadcrumbOnClick?: (...args: any[]) => any;
- /**
- * Interval time on search
- */
- debounceInterval?: number;
- /**
- * Disables treepicker including search bar
- */
+export interface TreePickerNodeProps {
+ node: TreePickerNodeNode;
+ children: React.ReactNode;
+ className?: string;
+}
+
+declare const TreePickerNode: React.FC & {
+ Content: typeof TreePickerNodeContent;
+ Expand: typeof TreePickerNodeExpand;
+ Add: typeof TreePickerNodeAdd;
+ Placeholder: typeof TreePickerNodePlaceholder;
+};
+
+export interface TreePickerNavProps {
+ rootLabel?: string;
+ className?: string;
+ onNavTo?: (...args: any[]) => any;
+}
+
+declare const TreePickerNav: React.FC;
+
+export interface TreePickerHeaderProps {
+ children?: React.ReactNode;
+ className?: string;
+}
+
+declare const TreePickerHeader: React.FC;
+
+export interface TreePickerSearchProps {
+ resolveNodes: (...args: any[]) => any;
+ className?: string;
disabled?: boolean;
- /**
- * Disables treepicker's grid item
- */
- disableInclude?: boolean;
- /**
- * The svg symbol used when there will be no item on both left or right Grid
- */
- emptySvgSymbol?: React.ReactNode;
- /**
- * The svg symbol used when there will be no item on right Grid (Selected list)
- */
- emptySelectedListSvgSymbol?: React.ReactNode;
- /**
- * Displays this text when there will be no item on left Grid. Prefer type 'string', but rich text can be used here
- */
- emptyText?: React.ReactNode;
- /**
- * Displays this text when there will be no item on right Grid(Selected list). Prefer type 'string', but rich text can be used here.
- */
- emptySelectedListText?: React.ReactNode;
- /**
- * Triggers when clicking any item in the left Grid
- */
- expandNode?: (...args: any[]) => any;
- /**
- * This function use to transform keys of the list item in the left Grid
- */
- groupFormatter?: (...args: any[]) => any;
- /**
- * Hides the empty icon on right Grid (Selected list). Given emptySvgSymbol and hideIcon together, the empty symbol will be only displayed on the left grid.
- */
- hideIcon?: boolean;
- /**
- * Click event on '+' button of each list Item
- */
- includeNode?: (...args: any[]) => any;
- /**
- * Same as emptyText
- */
- initialStateNode?: React.ReactNode;
- /**
- * Same as emptySymbol
- */
- initialStateSymbol?: React.ReactNode;
- /**
- * Uses for specific className
- */
- itemType?: string;
- isLoading?: boolean;
- /**
- * Uses for rendering custom node
- */
- nodeRenderer?: (...args: any[]) => any;
- removeNode?: (...args: any[]) => any;
- /**
- * Triggers when search input changes
- */
- onChange?: (...args: any[]) => any;
- /**
- * Triggers when the user clicks the clear button on search input
- */
- onClear?: (...args: any[]) => any;
- /**
- * Please see Search
- */
- onSearch?: (...args: any[]) => any;
- /**
- * Please see Search
- */
- searchOnEnter?: boolean;
- searchPlaceholder?: string;
- searchValue?: string;
- selectedNodes: TreePickerSimplePureSelectedNodes[];
- /**
- * Show or hide the search field on the selection pane
- */
- showSearch?: boolean;
- /**
- * A list of available unselected nodes. This prop is not required, but an empty array is not allowed. At least one element is required in the array.
- */
- subtree?: TreePickerSimplePureSubtree[];
- svgSymbolCancel?: React.ReactNode;
- svgSymbolSearch?: React.ReactNode;
- /**
- * e.g: Default Group
- */
- displayGroupHeader?: boolean;
- hideSearchOnRoot?: boolean;
- /**
- * A react node to be rendered at the top of the right hand side pane. Generally we are expecting a search component.
- */
- selectedTopSearch?: React.ReactNode;
- addNodePopoverInfoProps?: Object;
- removeNodePopoverInfoProps?: Object;
- breadcrumbRootNode?: TreePickerSimplePureBreadcrumbRootNode;
}
-declare const TreePickerSimplePure: React.FC;
+declare const TreePickerSearch: React.FC;
+
+export interface TreePickerTreeProps {
+ resolveRootNodes: (...args: any[]) => any;
+ className?: string;
+ hiddenNodeIds?: string[];
+ placeholder?: React.ReactNode;
+ emptyState?: React.ReactNode;
+}
+
+declare const TreePickerTree: React.FC;
+
+export interface TreePickerProps {
+ children: React.ReactNode;
+ renderNode: (...args: any[]) => any;
+ className?: string;
+}
+
+declare const TreePicker: React.FC & {
+ Node: typeof TreePickerNode;
+ Nav: typeof TreePickerNav;
+ Header: typeof TreePickerHeader;
+ Search: typeof TreePickerSearch;
+ Tree: typeof TreePickerTree;
+};
-export default TreePickerSimplePure;
+export default TreePicker;
diff --git a/src/components/TreePicker/index.js b/src/components/TreePicker/index.js
new file mode 100644
index 000000000..1110f33b1
--- /dev/null
+++ b/src/components/TreePicker/index.js
@@ -0,0 +1,11 @@
+export {
+ useTreePickerActions,
+ useTreePickerPaths,
+ useTreePickerNodes,
+ useTreePickerSearch,
+ useTreePickerCurrentNode,
+ useTreePickerGetState,
+ useTreePickerSlice,
+} from './TreePickerContext';
+export { useTreePickerNode } from './TreePickerNode';
+export { default } from './TreePicker';
diff --git a/src/components/TreePicker/Grid/index.d.ts b/src/components/TreePickerSimplePure/Grid/index.d.ts
similarity index 100%
rename from src/components/TreePicker/Grid/index.d.ts
rename to src/components/TreePickerSimplePure/Grid/index.d.ts
diff --git a/src/components/TreePicker/Grid/index.jsx b/src/components/TreePickerSimplePure/Grid/index.jsx
similarity index 100%
rename from src/components/TreePicker/Grid/index.jsx
rename to src/components/TreePickerSimplePure/Grid/index.jsx
diff --git a/src/components/TreePicker/Grid/index.spec.jsx b/src/components/TreePickerSimplePure/Grid/index.spec.jsx
similarity index 100%
rename from src/components/TreePicker/Grid/index.spec.jsx
rename to src/components/TreePickerSimplePure/Grid/index.spec.jsx
diff --git a/src/components/TreePicker/Grid/styles.css b/src/components/TreePickerSimplePure/Grid/styles.css
similarity index 100%
rename from src/components/TreePicker/Grid/styles.css
rename to src/components/TreePickerSimplePure/Grid/styles.css
diff --git a/src/components/TreePicker/Nav/index.d.ts b/src/components/TreePickerSimplePure/Nav/index.d.ts
similarity index 100%
rename from src/components/TreePicker/Nav/index.d.ts
rename to src/components/TreePickerSimplePure/Nav/index.d.ts
diff --git a/src/components/TreePicker/Nav/index.jsx b/src/components/TreePickerSimplePure/Nav/index.jsx
similarity index 100%
rename from src/components/TreePicker/Nav/index.jsx
rename to src/components/TreePickerSimplePure/Nav/index.jsx
diff --git a/src/components/TreePicker/Nav/index.spec.jsx b/src/components/TreePickerSimplePure/Nav/index.spec.jsx
similarity index 100%
rename from src/components/TreePicker/Nav/index.spec.jsx
rename to src/components/TreePickerSimplePure/Nav/index.spec.jsx
diff --git a/src/components/TreePicker/Nav/styles.css b/src/components/TreePickerSimplePure/Nav/styles.css
similarity index 100%
rename from src/components/TreePicker/Nav/styles.css
rename to src/components/TreePickerSimplePure/Nav/styles.css
diff --git a/src/components/TreePicker/Node/Expander/index.d.ts b/src/components/TreePickerSimplePure/Node/Expander/index.d.ts
similarity index 100%
rename from src/components/TreePicker/Node/Expander/index.d.ts
rename to src/components/TreePickerSimplePure/Node/Expander/index.d.ts
diff --git a/src/components/TreePicker/Node/Expander/index.jsx b/src/components/TreePickerSimplePure/Node/Expander/index.jsx
similarity index 100%
rename from src/components/TreePicker/Node/Expander/index.jsx
rename to src/components/TreePickerSimplePure/Node/Expander/index.jsx
diff --git a/src/components/TreePicker/Node/Expander/index.spec.jsx b/src/components/TreePickerSimplePure/Node/Expander/index.spec.jsx
similarity index 100%
rename from src/components/TreePicker/Node/Expander/index.spec.jsx
rename to src/components/TreePickerSimplePure/Node/Expander/index.spec.jsx
diff --git a/src/components/TreePicker/Node/index.d.ts b/src/components/TreePickerSimplePure/Node/index.d.ts
similarity index 100%
rename from src/components/TreePicker/Node/index.d.ts
rename to src/components/TreePickerSimplePure/Node/index.d.ts
diff --git a/src/components/TreePicker/Node/index.jsx b/src/components/TreePickerSimplePure/Node/index.jsx
similarity index 100%
rename from src/components/TreePicker/Node/index.jsx
rename to src/components/TreePickerSimplePure/Node/index.jsx
diff --git a/src/components/TreePicker/Node/index.spec.jsx b/src/components/TreePickerSimplePure/Node/index.spec.jsx
similarity index 100%
rename from src/components/TreePicker/Node/index.spec.jsx
rename to src/components/TreePickerSimplePure/Node/index.spec.jsx
diff --git a/src/components/TreePicker/Node/styles.css b/src/components/TreePickerSimplePure/Node/styles.css
similarity index 100%
rename from src/components/TreePicker/Node/styles.css
rename to src/components/TreePickerSimplePure/Node/styles.css
diff --git a/src/components/TreePickerSimplePure/TreePicker.stories.tsx b/src/components/TreePickerSimplePure/TreePicker.stories.tsx
new file mode 100644
index 000000000..da949dcec
--- /dev/null
+++ b/src/components/TreePickerSimplePure/TreePicker.stories.tsx
@@ -0,0 +1,114 @@
+import React from 'react';
+import type { Meta, StoryObj } from '@storybook/react';
+import _ from 'lodash';
+
+import TreePicker from './index';
+import SvgSymbol from '../SvgSymbol';
+import Search from '../Search';
+
+const meta = {
+ title: 'Pending Review/TreePicker',
+ component: TreePicker,
+ tags: ['autodocs'],
+} satisfies Meta;
+
+export default meta;
+
+type Story = StoryObj;
+
+const DefaultComponent = () => {
+ const [selectedSearchValue, setSelectedSearchValue] = React.useState('');
+ const [selectedNodes, setSelectedNodes] = React.useState([]);
+ const [pickerSearchValue, setPickerSearchValue] = React.useState('');
+ const subTree = [
+ {
+ id: '0',
+ label: 'Northern Territory',
+ path: [{ id: '10', label: 'Australia' }],
+ type: 'Territory',
+ },
+ {
+ id: '1',
+ label: 'Australian Capital Territory',
+ path: [{ id: '10', label: 'Australia' }],
+ type: 'Territory',
+ },
+ {
+ id: '2',
+ label: 'Victoria',
+ path: [{ id: '10', label: 'Australia' }],
+ type: 'State',
+ },
+ ];
+
+ const getSelectedNodes = () => {
+ if (_.isEmpty(selectedSearchValue)) return selectedNodes;
+
+ return _.filter(selectedNodes, ({ label }) => _.includes(label.toLowerCase(), selectedSearchValue.toLowerCase()));
+ };
+
+ const getSubtree = () => {
+ let treePickerPureSubtree = [];
+
+ // filter out nodes that do not contain search string
+ if (!_.isEmpty(pickerSearchValue)) {
+ treePickerPureSubtree = _.filter(subTree, ({ label }) =>
+ _.includes(label.toLowerCase(), pickerSearchValue.toLowerCase())
+ );
+ }
+
+ // filter out nodes that do not contain the selected search string
+ // however keep the nodes that are not selected but do not contain the selected search string
+ if (!_.isEmpty(selectedSearchValue)) {
+ treePickerPureSubtree = _.filter(treePickerPureSubtree, ({ id, label }) => {
+ if (_.find(selectedNodes, { id })) {
+ return _.includes(label.toLowerCase(), selectedSearchValue.toLowerCase());
+ }
+
+ return true;
+ });
+ }
+
+ return treePickerPureSubtree;
+ };
+
+ return (
+
+
+ }
+ emptySelectedListText={
+
+ Choose items of interest
+
+ }
+ initialStateNode={
+
+ Start by searching for items
+
+ }
+ searchValue={pickerSearchValue}
+ onChange={setPickerSearchValue}
+ searchOnClear={() => setPickerSearchValue('')}
+ includeNode={(node) => setSelectedNodes(_.concat([], selectedNodes, node))}
+ removeNode={(node) => setSelectedNodes(_.reject(selectedNodes, { id: node.id }))}
+ additionalClassNames={pickerSearchValue ? undefined : ['background-highlighted', 'test-class']}
+ selectedTopSearch={
+
+
+
+ }
+ />
+
+ );
+};
+
+export const Default: Story = {
+ args: {},
+ render: () => DefaultComponent(),
+};
diff --git a/src/components/TreePickerSimplePure/index.d.ts b/src/components/TreePickerSimplePure/index.d.ts
new file mode 100644
index 000000000..bd2f4b623
--- /dev/null
+++ b/src/components/TreePickerSimplePure/index.d.ts
@@ -0,0 +1,167 @@
+import * as React from 'react';
+
+export interface TreePickerSimplePureBreadcrumbNodes {
+ id?: any;
+ label: string;
+}
+
+export interface TreePickerSimplePureSelectedNodes {
+ id?: any;
+ label: string;
+ isExpandable?: boolean;
+ path?: {
+ id?: any;
+ label: string;
+ }[];
+ ancestors?: {
+ id?: any;
+ label: string;
+ }[];
+ type: string;
+ value?: number;
+ accent?: 'warning' | 'success' | 'info' | 'error';
+}
+
+export interface TreePickerSimplePureSubtree {
+ id?: any;
+ label: string;
+ isExpandable?: boolean;
+ path?: {
+ id?: any;
+ label: string;
+ }[];
+ ancestors?: {
+ id?: any;
+ label: string;
+ }[];
+ type: string;
+ value?: number;
+ accent?: 'warning' | 'success' | 'info' | 'error';
+}
+
+export interface TreePickerSimplePureBreadcrumbRootNode {
+ id?: any;
+ label: string;
+}
+
+export interface TreePickerSimplePureProps {
+ /**
+ * Class Names for SplitPane component
+ */
+ additionalClassNames?: string[];
+ /**
+ * Returns node id. This prop is not required, but an empty array is not allowed. At least one element is required in the array.
+ */
+ breadcrumbNodes?: TreePickerSimplePureBreadcrumbNodes[];
+ /**
+ * This propType creates a list of breadcrumb node
+ */
+ breadcrumbOnClick?: (...args: any[]) => any;
+ /**
+ * Interval time on search
+ */
+ debounceInterval?: number;
+ /**
+ * Disables treepicker including search bar
+ */
+ disabled?: boolean;
+ /**
+ * Disables treepicker's grid item
+ */
+ disableInclude?: boolean;
+ /**
+ * The svg symbol used when there will be no item on both left or right Grid
+ */
+ emptySvgSymbol?: React.ReactNode;
+ /**
+ * The svg symbol used when there will be no item on right Grid (Selected list)
+ */
+ emptySelectedListSvgSymbol?: React.ReactNode;
+ /**
+ * Displays this text when there will be no item on left Grid. Prefer type 'string', but rich text can be used here
+ */
+ emptyText?: React.ReactNode;
+ /**
+ * Displays this text when there will be no item on right Grid(Selected list). Prefer type 'string', but rich text can be used here.
+ */
+ emptySelectedListText?: React.ReactNode;
+ /**
+ * Triggers when clicking any item in the left Grid
+ */
+ expandNode?: (...args: any[]) => any;
+ /**
+ * This function use to transform keys of the list item in the left Grid
+ */
+ groupFormatter?: (...args: any[]) => any;
+ /**
+ * Hides the empty icon on right Grid (Selected list). Given emptySvgSymbol and hideIcon together, the empty symbol will be only displayed on the left grid.
+ */
+ hideIcon?: boolean;
+ /**
+ * Click event on '+' button of each list Item
+ */
+ includeNode?: (...args: any[]) => any;
+ /**
+ * Same as emptyText
+ */
+ initialStateNode?: React.ReactNode;
+ /**
+ * Same as emptySymbol
+ */
+ initialStateSymbol?: React.ReactNode;
+ /**
+ * Uses for specific className
+ */
+ itemType?: string;
+ isLoading?: boolean;
+ /**
+ * Uses for rendering custom node
+ */
+ nodeRenderer?: (...args: any[]) => any;
+ removeNode?: (...args: any[]) => any;
+ /**
+ * Triggers when search input changes
+ */
+ onChange?: (...args: any[]) => any;
+ /**
+ * Triggers when the user clicks the clear button on search input
+ */
+ onClear?: (...args: any[]) => any;
+ /**
+ * Please see Search
+ */
+ onSearch?: (...args: any[]) => any;
+ /**
+ * Please see Search
+ */
+ searchOnEnter?: boolean;
+ searchPlaceholder?: string;
+ searchValue?: string;
+ selectedNodes: TreePickerSimplePureSelectedNodes[];
+ /**
+ * Show or hide the search field on the selection pane
+ */
+ showSearch?: boolean;
+ /**
+ * A list of available unselected nodes. This prop is not required, but an empty array is not allowed. At least one element is required in the array.
+ */
+ subtree?: TreePickerSimplePureSubtree[];
+ svgSymbolCancel?: React.ReactNode;
+ svgSymbolSearch?: React.ReactNode;
+ /**
+ * e.g: Default Group
+ */
+ displayGroupHeader?: boolean;
+ hideSearchOnRoot?: boolean;
+ /**
+ * A react node to be rendered at the top of the right hand side pane. Generally we are expecting a search component.
+ */
+ selectedTopSearch?: React.ReactNode;
+ addNodePopoverInfoProps?: Object;
+ removeNodePopoverInfoProps?: Object;
+ breadcrumbRootNode?: TreePickerSimplePureBreadcrumbRootNode;
+}
+
+declare const TreePickerSimplePure: React.FC;
+
+export default TreePickerSimplePure;
diff --git a/src/components/TreePicker/index.jsx b/src/components/TreePickerSimplePure/index.jsx
similarity index 100%
rename from src/components/TreePicker/index.jsx
rename to src/components/TreePickerSimplePure/index.jsx
diff --git a/src/components/TreePicker/index.spec.jsx b/src/components/TreePickerSimplePure/index.spec.jsx
similarity index 100%
rename from src/components/TreePicker/index.spec.jsx
rename to src/components/TreePickerSimplePure/index.spec.jsx
diff --git a/src/components/TreePicker/mocks.js b/src/components/TreePickerSimplePure/mocks.js
similarity index 100%
rename from src/components/TreePicker/mocks.js
rename to src/components/TreePickerSimplePure/mocks.js
diff --git a/src/components/TreePicker/styles.css b/src/components/TreePickerSimplePure/styles.css
similarity index 100%
rename from src/components/TreePicker/styles.css
rename to src/components/TreePickerSimplePure/styles.css
diff --git a/src/hooks/index.js b/src/hooks/index.js
deleted file mode 100644
index 03e1ec0b4..000000000
--- a/src/hooks/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export { default as useArrowFocus } from './useArrowFocus';
diff --git a/src/hooks/useCallbackRef.js b/src/hooks/useCallbackRef.js
new file mode 100644
index 000000000..7852648a5
--- /dev/null
+++ b/src/hooks/useCallbackRef.js
@@ -0,0 +1,13 @@
+import React from 'react';
+
+const useCallbackRef = (callback) => {
+ const ref = React.useRef(callback);
+
+ React.useLayoutEffect(() => {
+ ref.current = callback;
+ });
+
+ return ref;
+};
+
+export default useCallbackRef;
diff --git a/src/hooks/useIsUnmounted.js b/src/hooks/useIsUnmounted.js
new file mode 100644
index 000000000..2b8b21868
--- /dev/null
+++ b/src/hooks/useIsUnmounted.js
@@ -0,0 +1,16 @@
+import React from 'react';
+
+const useIsUnmounted = () => {
+ const isUnmounted = React.useRef(false);
+
+ React.useEffect(() => {
+ isUnmounted.current = false;
+ return () => {
+ isUnmounted.current = true;
+ };
+ }, []);
+
+ return React.useCallback(() => isUnmounted.current, []);
+};
+
+export default useIsUnmounted;
diff --git a/src/index.d.ts b/src/index.d.ts
index 38e0347fa..c55eb6006 100644
--- a/src/index.d.ts
+++ b/src/index.d.ts
@@ -60,9 +60,10 @@ export { default as Tile } from './components/Tile';
export { default as TileGrid } from './components/TileGrid';
export { default as Toast } from './components/Toast';
export { default as Totals } from './components/Totals';
-export { default as TreePickerSimplePure } from './components/TreePicker';
-export { default as TreePickerGrid } from './components/TreePicker/Grid';
-export { default as TreePickerNav } from './components/TreePicker/Nav';
-export { default as TreePickerNode } from './components/TreePicker/Node';
+export { default as TreePicker } from './components/TreePicker';
+export { default as TreePickerSimplePure } from './components/TreePickerSimplePure';
+export { default as TreePickerGrid } from './components/TreePickerSimplePure/Grid';
+export { default as TreePickerNav } from './components/TreePickerSimplePure/Nav';
+export { default as TreePickerNode } from './components/TreePickerSimplePure/Node';
export { default as UserListPicker } from './components/UserListPicker';
export { default as VerticalNav } from './components/VerticalNav';
diff --git a/src/index.js b/src/index.js
index bea4c3f34..f9c49479d 100644
--- a/src/index.js
+++ b/src/index.js
@@ -64,10 +64,21 @@ import Tile from './components/Tile';
import TileGrid from './components/TileGrid';
import Toast from './components/Toast';
import Totals from './components/Totals';
-import TreePickerSimplePure from './components/TreePicker';
-import TreePickerGrid from './components/TreePicker/Grid';
-import TreePickerNav from './components/TreePicker/Nav';
-import TreePickerNode from './components/TreePicker/Node';
+import TreePicker, {
+ useTreePickerActions,
+ useTreePickerPaths,
+ useTreePickerNodes,
+ useTreePickerSearch,
+ useTreePickerCurrentNode,
+ useTreePickerGetState,
+ useTreePickerSlice,
+ useTreePickerNode,
+} from './components/TreePicker';
+// legacy TreePickerSimplePure will be deprecated once all usage replaced with TreePicker
+import TreePickerSimplePure from './components/TreePickerSimplePure';
+import TreePickerGrid from './components/TreePickerSimplePure/Grid';
+import TreePickerNav from './components/TreePickerSimplePure/Nav';
+import TreePickerNode from './components/TreePickerSimplePure/Node';
import UserListPicker from './components/UserListPicker';
import VerticalNav from './components/VerticalNav';
@@ -128,6 +139,15 @@ export {
TextEllipsis,
TileGrid,
Totals,
+ TreePicker,
+ useTreePickerActions,
+ useTreePickerPaths,
+ useTreePickerNodes,
+ useTreePickerSearch,
+ useTreePickerCurrentNode,
+ useTreePickerGetState,
+ useTreePickerSlice,
+ useTreePickerNode,
TreePickerGrid,
TreePickerNav,
TreePickerNode,
diff --git a/src/styles/icons/folder-open.svg b/src/styles/icons/folder-open.svg
new file mode 100644
index 000000000..100166e02
--- /dev/null
+++ b/src/styles/icons/folder-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/styles/icons/folder.svg b/src/styles/icons/folder.svg
new file mode 100644
index 000000000..5f749c05d
--- /dev/null
+++ b/src/styles/icons/folder.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/styles/icons/plus.svg b/src/styles/icons/plus.svg
new file mode 100644
index 000000000..0f4db63c0
--- /dev/null
+++ b/src/styles/icons/plus.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/svgo-config.js b/svgo-config.js
index 84b37ecb0..734dff2c1 100644
--- a/svgo-config.js
+++ b/svgo-config.js
@@ -10,6 +10,8 @@ module.exports = {
},
removeDoctype: false,
+
+ removeViewBox: false,
},
},
},
@@ -22,5 +24,6 @@ module.exports = {
attrs: ['id'],
},
},
+ { name: 'removeDimensions' },
],
};