@carbon/react
Version:
React components for the Carbon Design System
159 lines (151 loc) • 5.28 kB
JavaScript
/**
* Copyright IBM Corp. 2016, 2023
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/
import { ChevronDown } from '@carbon/icons-react';
import cx from 'classnames';
import PropTypes from 'prop-types';
import React, { useContext, useState } from 'react';
import SideNavIcon from './SideNavIcon.js';
import { Escape } from '../../internal/keyboard/keys.js';
import { match } from '../../internal/keyboard/match.js';
import { usePrefix } from '../../internal/usePrefix.js';
import { SideNavContext } from './SideNav.js';
var _ChevronDown;
const SideNavMenu = /*#__PURE__*/React.forwardRef(function SideNavMenu({
className: customClassName,
children,
defaultExpanded = false,
isActive = false,
large = false,
renderIcon: IconElement,
isSideNavExpanded,
tabIndex,
title
}, ref) {
const {
isRail
} = useContext(SideNavContext);
const prefix = usePrefix();
const [isExpanded, setIsExpanded] = useState(defaultExpanded);
const [prevExpanded, setPrevExpanded] = useState(defaultExpanded);
const className = cx({
[`${prefix}--side-nav__item`]: true,
[`${prefix}--side-nav__item--active`]: isActive || hasActiveDescendant(children) && !isExpanded,
[`${prefix}--side-nav__item--icon`]: IconElement,
[`${prefix}--side-nav__item--large`]: large,
[customClassName]: !!customClassName
});
if (!isSideNavExpanded && isExpanded && isRail) {
setIsExpanded(false);
setPrevExpanded(true);
} else if (isSideNavExpanded && prevExpanded && isRail) {
setIsExpanded(true);
setPrevExpanded(false);
}
return (
/*#__PURE__*/
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
React.createElement("li", {
className: className,
onKeyDown: event => {
if (match(event, Escape)) {
setIsExpanded(false);
}
}
}, /*#__PURE__*/React.createElement("button", {
"aria-expanded": isExpanded,
className: `${prefix}--side-nav__submenu`,
onClick: () => {
setIsExpanded(!isExpanded);
},
ref: ref,
type: "button",
tabIndex: tabIndex === undefined ? !isSideNavExpanded && !isRail ? -1 : 0 : tabIndex
}, IconElement && /*#__PURE__*/React.createElement(SideNavIcon, null, /*#__PURE__*/React.createElement(IconElement, null)), /*#__PURE__*/React.createElement("span", {
className: `${prefix}--side-nav__submenu-title`
}, title), /*#__PURE__*/React.createElement(SideNavIcon, {
className: `${prefix}--side-nav__submenu-chevron`,
small: true
}, _ChevronDown || (_ChevronDown = /*#__PURE__*/React.createElement(ChevronDown, {
size: 20
})))), /*#__PURE__*/React.createElement("ul", {
className: `${prefix}--side-nav__menu`
}, children))
);
});
SideNavMenu.displayName = 'SideNavMenu';
SideNavMenu.propTypes = {
/**
* Provide <SideNavMenuItem>'s inside of the `SideNavMenu`
*/
children: PropTypes.node,
/**
* Provide an optional class to be applied to the containing node
*/
className: PropTypes.string,
/**
* Specify whether the menu should default to expanded. By default, it will
* be closed.
*/
defaultExpanded: PropTypes.bool,
/**
* Specify whether the `SideNavMenu` is "active". `SideNavMenu` should be
* considered active if one of its menu items are a link for the current
* page.
*/
isActive: PropTypes.bool,
/**
* Property to indicate if the side nav container is open (or not). Use to
* keep local state and styling in step with the SideNav expansion state.
*/
isSideNavExpanded: PropTypes.bool,
/**
* Specify if this is a large variation of the SideNavMenu
*/
large: PropTypes.bool,
/**
* A component used to render an icon.
*/
renderIcon: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
/**
* Optional prop to specify the tabIndex of the button. If undefined, it will be applied default validation
*/
tabIndex: PropTypes.number,
/**
* Provide the text for the overall menu name
*/
title: PropTypes.string.isRequired
};
/**
Defining the children parameter with the type ReactNode | ReactNode[]. This allows for various possibilities:
a single element, an array of elements, or null or undefined.
**/
function hasActiveDescendant(children) {
if (Array.isArray(children)) {
return children.some(child => {
if (! /*#__PURE__*/React.isValidElement(child)) {
return false;
}
/** Explicitly defining the expected prop types (isActive and 'aria-current) for the children to ensure type
safety when accessing their props.
**/
const props = child.props;
if (props.isActive === true || props['aria-current'] || props.children instanceof Array && hasActiveDescendant(props.children)) {
return true;
}
return false;
});
}
// We use React.isValidElement(child) to check if the child is a valid React element before accessing its props
if (/*#__PURE__*/React.isValidElement(children)) {
const props = children.props;
if (props.isActive === true || props['aria-current']) {
return true;
}
}
return false;
}
export { SideNavMenu, SideNavMenu as default };