Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: add typescript types to TreeNode and TreeView #14775

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .all-contributorsrc
Original file line number Diff line number Diff line change
Expand Up @@ -1298,6 +1298,16 @@
"code"
]
},
{
"login": "cesardlinx",
"name": "David Padilla",
"avatar_url": "https://avatars.githubusercontent.com/u/25573926?v=4",
"profile": "https://www.davidpadilla.dev/",
"contributions": [
"code"
]
},
{
"login": "allisonishida",
"name": "Allison Ishida",
"avatar_url": "https://avatars.githubusercontent.com/u/22247062?v=4",
Expand Down
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -259,12 +259,11 @@ check out our [Contributing Guide](/.github/CONTRIBUTING.md) and our
</tr>
<tr>
<td align="center"><a href="https://github.com/Nirajsah"><img src="https://avatars.githubusercontent.com/u/51414373?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Niraj Sah</b></sub></a><br /><a href="https://github.com/carbon-design-system/carbon/commits?author=Nirajsah" title="Code">💻</a></td>
<td align="center"><a href="https://www.davidpadilla.dev/"><img src="https://avatars.githubusercontent.com/u/25573926?v=4?s=100" width="100px;" alt=""/><br /><sub><b>David Padilla</b></sub></a><br /><a href="https://github.com/carbon-design-system/carbon/commits?author=cesardlinx" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/allisonishida"><img src="https://avatars.githubusercontent.com/u/22247062?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Allison Ishida</b></sub></a><br /><a href="https://github.com/carbon-design-system/carbon/commits?author=allisonishida" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/alewitt2"><img src="https://avatars.githubusercontent.com/u/48691328?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Alex Lewitt</b></sub></a><br /><a href="https://github.com/carbon-design-system/carbon/commits?author=alewitt2" title="Code">💻</a></td>
<td align="center"><a href="https://haruki-kuriwada.netlify.app/"><img src="https://avatars.githubusercontent.com/u/62743644?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Ruki</b></sub></a><br /><a href="https://github.com/carbon-design-system/carbon/commits?author=kuri-sun" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/allisonishida"><img src="https://avatars.githubusercontent.com/u/22247062?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Allison Ishida</b></sub></a><br /><a href="https://github.com/carbon-design-system/carbon/commits?author=allisonishida" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/Tresau-IBM"><img src="https://avatars.githubusercontent.com/u/148357638?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Tresau-IBM</b></sub></a><br /><a href="https://github.com/carbon-design-system/carbon/commits?author=Tresau-IBM" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/alewitt2"><img src="https://avatars.githubusercontent.com/u/48691328?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Alex Lewitt</b></sub></a><br /><a href="https://github.com/carbon-design-system/carbon/commits?author=alewitt2" title="Code">💻</a></td>
<td align="center"><a href="https://haruki-kuriwada.netlify.app/"><img src="https://avatars.githubusercontent.com/u/62743644?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Ruki</b></sub></a><br /><a href="https://github.com/carbon-design-system/carbon/commits?author=kuri-sun" title="Code">💻</a></td>
</tr>
</table>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,163 @@
* LICENSE file in the root directory of this source tree.
*/

import React, { useState, useEffect, useRef } from 'react';
import React, { useState, useEffect, useRef, RefObject } from 'react';
import PropTypes from 'prop-types';
import { CaretDown } from '@carbon/icons-react';
import classNames from 'classnames';
import { keys, match, matches } from '../../internal/keyboard';
import uniqueId from '../../tools/uniqueId';
import { usePrefix } from '../../internal/usePrefix';

const TreeNode = React.forwardRef(
export interface SelectedNode {
id: string | number;
label?: React.ReactNode;
value: string;
activeNodeId?: string | number;
}

interface TreeNodeProps {
/**
* Node ID
*/
id?: string | number;

/**
* The value of the active node in the tree
*/
active?: string | number;

/**
* Specify the children of the TreeNode
*/
children?: React.ReactNode;

/**
* Specify an optional className to be applied to the TreeNode
*/
className?: string;

/**
* TreeNode depth to determine spacing, automatically calculated by default
*/
depth?: number;

/**
* Specify if the TreeNode is disabled
*/
disabled?: boolean;

/**
* Specify if the TreeNode is expanded (only applicable to parent nodes)
*/
isExpanded?: boolean;

/**
* Rendered label for the TreeNode
*/
label?: React.ReactNode;

/**
* Callback function for when the node receives or loses focus
*/
onNodeFocusEvent?: (event: React.FocusEvent<HTMLElement>) => void;

/**
* Callback function for when the node is selected
*/
onSelect?: (
event: React.MouseEvent<HTMLButtonElement>,
{
id,
label,
value,
}: { id: string | number; label: React.ReactNode; value: string }
) => void;

/**
* Callback function for when a parent node is expanded or collapsed
*/
onToggle?: (
event: React.MouseEvent<HTMLButtonElement>,
{
id,
isExpanded,
label,
value,
}: {
id: string | number;
isExpanded: boolean;
label?: React.ReactNode;
value: string;
}
) => void;

/**
* Callback function for when any node in the tree is selected
*/
onTreeSelect?: (
event: React.MouseEvent<HTMLButtonElement>,
node: SelectedNode
) => void;

onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
onKeyDown?: (event: React.KeyboardEvent<HTMLImageElement>) => void;
onBlur?: (event: React.FocusEvent<HTMLInputElement>) => void;
onFocus?: (event: React.FocusEvent<HTMLInputElement>) => void;

/**
* Optional prop to allow each node to have an associated icon.
* Can be a React component class
*/
renderIcon?: object | null;

/**
* Array containing all selected node IDs in the tree
*/
selected?: (string | number | null | undefined)[];

/**
* Specify the value of the TreeNode
*/
value?: string;

/**
* Indicates that this element represents the current
* item within a container or set of related elements.
*/
'aria-current'?:
| boolean
| 'time'
| 'false'
| 'true'
| 'page'
| 'step'
| 'location'
| 'date'
| null;

/**
* Indicates the current "selected" state of the tree node
*/
'aria-selected'?: boolean | null;

/**
* Indicates that the element is perceivable but disabled
*/
'aria-disabled'?: boolean;

/**
* Name of a role in the ARIA specification
*/
role?: string;

/**
* Current TreeNode
*/
ref?: RefObject<HTMLLIElement>;
}

const TreeNode = React.forwardRef<HTMLLIElement, TreeNodeProps>(
(
{
active,
Expand All @@ -36,23 +184,23 @@ const TreeNode = React.forwardRef(
) => {
const { current: id } = useRef(rest.id || uniqueId());
const [expanded, setExpanded] = useState(isExpanded);
const currentNode = useRef(null);
const currentNodeLabel = useRef(null);
const currentNode = useRef<HTMLLIElement>(null);
const currentNodeLabel = useRef<HTMLDivElement>(null);
const prefix = usePrefix();
const nodesWithProps = React.Children.map(children, (node) => {
if (React.isValidElement(node)) {
return React.cloneElement(node, {
active,
depth: depth + 1,
depth: (depth ?? 0) + 1,
disabled,
onTreeSelect,
selected,
tabIndex: (!node.props.disabled && -1) || null,
});
} as TreeNodeProps);
}
});
const isActive = active === id;
const isSelected = selected.includes(id);
const isSelected = selected?.includes(id);
const treeNodeClasses = classNames(className, `${prefix}--tree-node`, {
[`${prefix}--tree-node--active`]: isActive,
[`${prefix}--tree-node--disabled`]: disabled,
Expand All @@ -71,14 +219,19 @@ const TreeNode = React.forwardRef(
if (disabled) {
return;
}
onToggle?.(event, { id, isExpanded: !expanded, label, value });
onToggle?.(event, {
id,
isExpanded: !expanded,
label,
value: value as string,
});
setExpanded(!expanded);
}
function handleClick(event) {
event.stopPropagation();
if (!disabled) {
onTreeSelect?.(event, { id, label, value });
onNodeSelect?.(event, { id, label, value });
onTreeSelect?.(event, { id, label, value: value as string });
onNodeSelect?.(event, { id, label, value: value as string });
rest?.onClick?.(event);
}
}
Expand All @@ -100,14 +253,19 @@ const TreeNode = React.forwardRef(
return findParentTreeNode(node.parentNode);
};
if (children && expanded) {
onToggle?.(event, { id, isExpanded: false, label, value });
onToggle?.(event, {
id,
isExpanded: false,
label,
value: value as string,
});
setExpanded(false);
} else {
/**
* When focus is on a leaf node or a closed parent node, move focus to
* its parent node (unless its depth is level 1)
*/
findParentTreeNode(currentNode.current.parentNode)?.focus();
findParentTreeNode(currentNode.current?.parentNode)?.focus();
}
}
if (children && match(event, keys.ArrowRight)) {
Expand All @@ -116,9 +274,14 @@ const TreeNode = React.forwardRef(
* When focus is on an expanded parent node, move focus to the first
* child node
*/
currentNode.current.lastChild.firstChild.focus();
(currentNode.current?.lastChild?.firstChild as HTMLElement)?.focus();
} else {
onToggle?.(event, { id, isExpanded: true, label, value });
onToggle?.(event, {
id,
isExpanded: true,
label,
value: value as string,
});
setExpanded(true);
}
}
Expand Down Expand Up @@ -151,20 +314,22 @@ const TreeNode = React.forwardRef(
* reduced spacing between the expand icon and the node icon + label)
*/
const calcOffset = () => {
// parent node with icon
if (children && Icon) {
return depth + 1 + depth * 0.5;
}
// parent node without icon
if (children) {
return depth + 1;
}
// leaf node with icon
if (Icon) {
return depth + 2 + depth * 0.5;
if (depth != null) {
tay1orjones marked this conversation as resolved.
Show resolved Hide resolved
// parent node with icon
if (children && Icon) {
return depth + 1 + depth * 0.5;
}
// parent node without icon
if (children) {
return depth + 1;
}
// leaf node with icon
if (Icon) {
return depth + 2 + depth * 0.5;
}
// leaf node without icon
return depth + 2.5;
}
// leaf node without icon
return depth + 2.5;
};

if (currentNodeLabel.current) {
Expand All @@ -191,30 +356,41 @@ const TreeNode = React.forwardRef(
role: 'treeitem',
};

const TreeIcon = Icon as React.FunctionComponent<{ className: string }>;
if (!children) {
return (
<li {...treeNodeProps}>
// eslint-disable-next-line jsx-a11y/role-supports-aria-props
<li
{...treeNodeProps}
aria-current={treeNodeProps['aria-current'] || undefined}
aria-selected={treeNodeProps['aria-selected'] || undefined}
id={treeNodeProps.id as string}>
<div className={`${prefix}--tree-node__label`} ref={currentNodeLabel}>
{Icon && <Icon className={`${prefix}--tree-node__icon`} />}
{TreeIcon && <TreeIcon className={`${prefix}--tree-node__icon`} />}
{label}
</div>
</li>
);
}
return (
// eslint-disable-next-line jsx-a11y/role-supports-aria-props
<li {...treeNodeProps} aria-expanded={!!expanded} ref={ref}>
<li
{...treeNodeProps}
aria-expanded={!!expanded}
ref={ref}
aria-current={treeNodeProps['aria-current'] || undefined}
aria-selected={treeNodeProps['aria-selected'] || undefined}
id={treeNodeProps.id as string}>
tay1orjones marked this conversation as resolved.
Show resolved Hide resolved
<div className={`${prefix}--tree-node__label`} ref={currentNodeLabel}>
{/* https://github.com/carbon-design-system/carbon/pull/6008#issuecomment-675738670 */}
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
<span
className={`${prefix}--tree-parent-node__toggle`}
disabled={disabled}
onClick={handleToggleClick}>
<CaretDown className={toggleClasses} />
</span>
<span className={`${prefix}--tree-node__label__details`}>
{Icon && <Icon className={`${prefix}--tree-node__icon`} />}
{TreeIcon && <TreeIcon className={`${prefix}--tree-node__icon`} />}
{label}
</span>
</div>
Expand Down
Loading
Loading