Skip to content

Commit

Permalink
Merge pull request #180 from dgreene1/feature/UIEN-5890
Browse files Browse the repository at this point in the history
Abilities to set focus by passing focusedId props
  • Loading branch information
mellis481 authored Apr 26, 2024
2 parents 8baaf0f + e305c11 commit 5aeca46
Show file tree
Hide file tree
Showing 10 changed files with 496 additions and 29 deletions.
1 change: 1 addition & 0 deletions src/TreeView/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export const treeTypes = {
changeSelectMany: "SELECT_MANY",
exclusiveChangeSelectMany: "EXCLUSIVE_CHANGE_SELECT_MANY",
focus: "FOCUS",
clearFocus: "CLEAR_FOCUS",
blur: "BLUR",
disable: "DISABLE",
enable: "ENABLE",
Expand Down
48 changes: 43 additions & 5 deletions src/TreeView/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
noop,
isBranchNotSelectedAndHasOnlySelectedChild,
getOnSelectTreeAction,
getBranchNodesToExpand,
} from "./utils";
import { Node } from "./node";
import {
Expand Down Expand Up @@ -62,6 +63,7 @@ interface IUseTreeProps {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onLoadData?: (props: ITreeViewOnLoadDataProps) => Promise<any>;
togglableSelect?: boolean;
focusedId?: NodeId;
}

const useTree = ({
Expand All @@ -82,6 +84,7 @@ const useTree = ({
propagateSelect,
propagateSelectUpwards,
treeRef,
focusedId,
}: IUseTreeProps) => {
const treeParentNode = getTreeParent(data);
const [state, dispatch] = useReducer(treeReducer, {
Expand Down Expand Up @@ -419,19 +422,47 @@ const useTree = ({
nodeRefs?.current != null &&
leafRefs?.current != null
) {
const isTreeActive = (treeRef?.current == null) ||
(document.activeElement && treeRef.current.contains(document.activeElement));
if (isTreeActive) {
const isTreeActive =
treeRef?.current == null ||
(document.activeElement &&
treeRef.current.contains(document.activeElement));
if (isTreeActive || focusedId) {
// Only scroll and focus on the tree when it is the active element on the page.
// This prevents controlled updates from scrolling to the tree and giving it focus.
const tabbableNode = nodeRefs.current[tabbableId];
const leafNode = leafRefs.current[lastInteractedWith];
const leafNode = leafRefs.current[lastInteractedWith];
scrollToRef(leafNode);
focusRef(tabbableNode);
}
}
}, [tabbableId, nodeRefs, leafRefs, lastInteractedWith]);

//Controlled focus
useEffect(() => {
if (!focusedId) {
dispatch({
type: treeTypes.clearFocus,
id: treeParentNode.children[0],
});
}

if (focusedId && data.find((node) => node.id === focusedId)) {
const nodesToExpand = getBranchNodesToExpand(data, focusedId);
if (nodesToExpand.length) {
dispatch({
type: treeTypes.expandMany,
ids: nodesToExpand,
lastInteractedWith: focusedId,
});
}
dispatch({
type: treeTypes.focus,
id: focusedId,
lastInteractedWith: focusedId,
});
}
}, [focusedId]);

// The "as const" technique tells Typescript that this is a tuple not an array
return [state, dispatch] as const;
};
Expand Down Expand Up @@ -518,6 +549,8 @@ export interface ITreeViewProps {
treeState: ITreeViewState;
dispatch: React.Dispatch<TreeViewAction>;
}) => void;
/** Id of the node to focus */
focusedId?: NodeId;
}

const TreeView = React.forwardRef<HTMLUListElement, ITreeViewProps>(
Expand All @@ -543,6 +576,7 @@ const TreeView = React.forwardRef<HTMLUListElement, ITreeViewProps>(
clickAction = clickActions.select,
nodeAction = "select",
expandedIds,
focusedId,
onBlur,
...other
},
Expand Down Expand Up @@ -572,7 +606,8 @@ const TreeView = React.forwardRef<HTMLUListElement, ITreeViewProps>(
multiSelect,
propagateSelect,
propagateSelectUpwards,
treeRef: innerRef,
treeRef: innerRef,
focusedId,
});
propagateSelect = propagateSelect && multiSelect;

Expand Down Expand Up @@ -1006,6 +1041,9 @@ TreeView.propTypes = {

/** Function called to load data asynchronously on expand */
onLoadData: PropTypes.func,

/** Id of the node to focus on */
focusedId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
};

export default TreeView;
8 changes: 8 additions & 0 deletions src/TreeView/reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export type TreeViewAction =
lastManuallyToggled?: NodeId | null;
}
| { type: "FOCUS"; id: NodeId; lastInteractedWith?: NodeId | null }
| { type: "CLEAR_FOCUS"; id: NodeId }
| { type: "BLUR" }
| { type: "DISABLE"; id: NodeId }
| { type: "ENABLE"; id: NodeId }
Expand Down Expand Up @@ -355,6 +356,13 @@ export const treeReducer = (
...state,
isFocused: false,
};
case treeTypes.clearFocus:
return {
...state,
isFocused: false,
lastInteractedWith: null,
tabbableId: action.id,
};
case treeTypes.disable: {
const disabledIds = new Set<NodeId>(state.disabledIds);
disabledIds.add(action.id);
Expand Down
17 changes: 16 additions & 1 deletion src/TreeView/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,18 @@ export const isBranchNode = (data: INode[], i: NodeId) => {
return !!node.children?.length;
};

export const getBranchNodesToExpand = (data: INode[], id: NodeId): NodeId[] => {
const parentId = getParent(data, id);
const isNodeExpandable =
parentId &&
(isBranchNode(data, parentId) || getTreeNode(data, parentId).isBranch);

if (!parentId || !isNodeExpandable) {
return [];
}
return [parentId, ...getBranchNodesToExpand(data, parentId)];
};

export const scrollToRef = (ref: INodeRef) => {
if (ref != null && ref.scrollIntoView) {
ref.scrollIntoView({ block: "nearest" });
Expand Down Expand Up @@ -290,7 +302,10 @@ export const getAccessibleRange = ({
/**
* This is to help consumers to understand that we do not currently support metadata that is a nested object. If this is needed, make an issue in Github
*/
export type IFlatMetadata = Record<string, string | number | boolean | undefined | null>;
export type IFlatMetadata = Record<
string,
string | number | boolean | undefined | null
>;

interface ITreeNode<M extends IFlatMetadata> {
id?: NodeId;
Expand Down
Loading

0 comments on commit 5aeca46

Please sign in to comment.