UNPKG

@carbon/react

Version:

React components for the Carbon Design System

159 lines (151 loc) 5.28 kB
/** * 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 };