Skip to content

Commit 5aeca46

Browse files
authored
Merge pull request #180 from dgreene1/feature/UIEN-5890
Abilities to set focus by passing focusedId props
2 parents 8baaf0f + e305c11 commit 5aeca46

File tree

10 files changed

+496
-29
lines changed

10 files changed

+496
-29
lines changed

src/TreeView/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export const treeTypes = {
3636
changeSelectMany: "SELECT_MANY",
3737
exclusiveChangeSelectMany: "EXCLUSIVE_CHANGE_SELECT_MANY",
3838
focus: "FOCUS",
39+
clearFocus: "CLEAR_FOCUS",
3940
blur: "BLUR",
4041
disable: "DISABLE",
4142
enable: "ENABLE",

src/TreeView/index.tsx

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import {
3333
noop,
3434
isBranchNotSelectedAndHasOnlySelectedChild,
3535
getOnSelectTreeAction,
36+
getBranchNodesToExpand,
3637
} from "./utils";
3738
import { Node } from "./node";
3839
import {
@@ -62,6 +63,7 @@ interface IUseTreeProps {
6263
// eslint-disable-next-line @typescript-eslint/no-explicit-any
6364
onLoadData?: (props: ITreeViewOnLoadDataProps) => Promise<any>;
6465
togglableSelect?: boolean;
66+
focusedId?: NodeId;
6567
}
6668

6769
const useTree = ({
@@ -82,6 +84,7 @@ const useTree = ({
8284
propagateSelect,
8385
propagateSelectUpwards,
8486
treeRef,
87+
focusedId,
8588
}: IUseTreeProps) => {
8689
const treeParentNode = getTreeParent(data);
8790
const [state, dispatch] = useReducer(treeReducer, {
@@ -419,19 +422,47 @@ const useTree = ({
419422
nodeRefs?.current != null &&
420423
leafRefs?.current != null
421424
) {
422-
const isTreeActive = (treeRef?.current == null) ||
423-
(document.activeElement && treeRef.current.contains(document.activeElement));
424-
if (isTreeActive) {
425+
const isTreeActive =
426+
treeRef?.current == null ||
427+
(document.activeElement &&
428+
treeRef.current.contains(document.activeElement));
429+
if (isTreeActive || focusedId) {
425430
// Only scroll and focus on the tree when it is the active element on the page.
426431
// This prevents controlled updates from scrolling to the tree and giving it focus.
427432
const tabbableNode = nodeRefs.current[tabbableId];
428-
const leafNode = leafRefs.current[lastInteractedWith];
433+
const leafNode = leafRefs.current[lastInteractedWith];
429434
scrollToRef(leafNode);
430435
focusRef(tabbableNode);
431436
}
432437
}
433438
}, [tabbableId, nodeRefs, leafRefs, lastInteractedWith]);
434439

440+
//Controlled focus
441+
useEffect(() => {
442+
if (!focusedId) {
443+
dispatch({
444+
type: treeTypes.clearFocus,
445+
id: treeParentNode.children[0],
446+
});
447+
}
448+
449+
if (focusedId && data.find((node) => node.id === focusedId)) {
450+
const nodesToExpand = getBranchNodesToExpand(data, focusedId);
451+
if (nodesToExpand.length) {
452+
dispatch({
453+
type: treeTypes.expandMany,
454+
ids: nodesToExpand,
455+
lastInteractedWith: focusedId,
456+
});
457+
}
458+
dispatch({
459+
type: treeTypes.focus,
460+
id: focusedId,
461+
lastInteractedWith: focusedId,
462+
});
463+
}
464+
}, [focusedId]);
465+
435466
// The "as const" technique tells Typescript that this is a tuple not an array
436467
return [state, dispatch] as const;
437468
};
@@ -518,6 +549,8 @@ export interface ITreeViewProps {
518549
treeState: ITreeViewState;
519550
dispatch: React.Dispatch<TreeViewAction>;
520551
}) => void;
552+
/** Id of the node to focus */
553+
focusedId?: NodeId;
521554
}
522555

523556
const TreeView = React.forwardRef<HTMLUListElement, ITreeViewProps>(
@@ -543,6 +576,7 @@ const TreeView = React.forwardRef<HTMLUListElement, ITreeViewProps>(
543576
clickAction = clickActions.select,
544577
nodeAction = "select",
545578
expandedIds,
579+
focusedId,
546580
onBlur,
547581
...other
548582
},
@@ -572,7 +606,8 @@ const TreeView = React.forwardRef<HTMLUListElement, ITreeViewProps>(
572606
multiSelect,
573607
propagateSelect,
574608
propagateSelectUpwards,
575-
treeRef: innerRef,
609+
treeRef: innerRef,
610+
focusedId,
576611
});
577612
propagateSelect = propagateSelect && multiSelect;
578613

@@ -1006,6 +1041,9 @@ TreeView.propTypes = {
10061041

10071042
/** Function called to load data asynchronously on expand */
10081043
onLoadData: PropTypes.func,
1044+
1045+
/** Id of the node to focus on */
1046+
focusedId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
10091047
};
10101048

10111049
export default TreeView;

src/TreeView/reducer.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ export type TreeViewAction =
6060
lastManuallyToggled?: NodeId | null;
6161
}
6262
| { type: "FOCUS"; id: NodeId; lastInteractedWith?: NodeId | null }
63+
| { type: "CLEAR_FOCUS"; id: NodeId }
6364
| { type: "BLUR" }
6465
| { type: "DISABLE"; id: NodeId }
6566
| { type: "ENABLE"; id: NodeId }
@@ -355,6 +356,13 @@ export const treeReducer = (
355356
...state,
356357
isFocused: false,
357358
};
359+
case treeTypes.clearFocus:
360+
return {
361+
...state,
362+
isFocused: false,
363+
lastInteractedWith: null,
364+
tabbableId: action.id,
365+
};
358366
case treeTypes.disable: {
359367
const disabledIds = new Set<NodeId>(state.disabledIds);
360368
disabledIds.add(action.id);

src/TreeView/utils.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,18 @@ export const isBranchNode = (data: INode[], i: NodeId) => {
5151
return !!node.children?.length;
5252
};
5353

54+
export const getBranchNodesToExpand = (data: INode[], id: NodeId): NodeId[] => {
55+
const parentId = getParent(data, id);
56+
const isNodeExpandable =
57+
parentId &&
58+
(isBranchNode(data, parentId) || getTreeNode(data, parentId).isBranch);
59+
60+
if (!parentId || !isNodeExpandable) {
61+
return [];
62+
}
63+
return [parentId, ...getBranchNodesToExpand(data, parentId)];
64+
};
65+
5466
export const scrollToRef = (ref: INodeRef) => {
5567
if (ref != null && ref.scrollIntoView) {
5668
ref.scrollIntoView({ block: "nearest" });
@@ -290,7 +302,10 @@ export const getAccessibleRange = ({
290302
/**
291303
* 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
292304
*/
293-
export type IFlatMetadata = Record<string, string | number | boolean | undefined | null>;
305+
export type IFlatMetadata = Record<
306+
string,
307+
string | number | boolean | undefined | null
308+
>;
294309

295310
interface ITreeNode<M extends IFlatMetadata> {
296311
id?: NodeId;

0 commit comments

Comments
 (0)