UNPKG

@carbon/react

Version:

React components for the Carbon Design System

118 lines (116 loc) 4.33 kB
/** * Copyright IBM Corp. 2016, 2026 * * 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 { usePrefix } from "../../internal/usePrefix.js"; import { Escape } from "../../internal/keyboard/keys.js"; import { match } from "../../internal/keyboard/match.js"; import { SideNavContext } from "./SideNavContext.js"; import SideNavIcon from "./SideNavIcon.js"; import classNames from "classnames"; import React, { forwardRef, useContext, useEffect, useRef, useState } from "react"; import PropTypes from "prop-types"; import { jsx, jsxs } from "react/jsx-runtime"; import { ChevronDown } from "@carbon/icons-react"; //#region src/components/UIShell/SideNavMenu.tsx /** * Copyright IBM Corp. 2016, 2026 * * This source code is licensed under the Apache-2.0 license found in the * LICENSE file in the root directory of this source tree. */ const SideNavMenu = forwardRef(({ className: customClassName, children, defaultExpanded = false, isActive = false, large = false, renderIcon: IconElement, isSideNavExpanded, tabIndex, title }, ref) => { const { isRail, isSideNavExpanded: contextIsSideNavExpanded } = useContext(SideNavContext); const currentIsSideNavExpanded = isSideNavExpanded ?? contextIsSideNavExpanded; const prefix = usePrefix(); const [isExpanded, setIsExpanded] = useState(defaultExpanded); const prevExpandedRef = useRef(false); const className = classNames({ [`${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 }); useEffect(() => { if (!isRail) return; if (!currentIsSideNavExpanded && isExpanded) { setIsExpanded(false); prevExpandedRef.current = true; } else if (currentIsSideNavExpanded && prevExpandedRef.current) { setIsExpanded(true); prevExpandedRef.current = false; } }, [ currentIsSideNavExpanded, isExpanded, isRail ]); return /* @__PURE__ */ jsxs("li", { className, onKeyDown: (event) => { if (match(event, Escape)) setIsExpanded(false); }, children: [/* @__PURE__ */ jsxs("button", { "aria-expanded": isExpanded, className: `${prefix}--side-nav__submenu`, onClick: () => { setIsExpanded(!isExpanded); }, ref, type: "button", tabIndex: tabIndex === void 0 ? !currentIsSideNavExpanded && !isRail ? -1 : 0 : tabIndex, children: [ IconElement && /* @__PURE__ */ jsx(SideNavIcon, { children: /* @__PURE__ */ jsx(IconElement, {}) }), /* @__PURE__ */ jsx("span", { className: `${prefix}--side-nav__submenu-title`, children: title }), /* @__PURE__ */ jsx(SideNavIcon, { className: `${prefix}--side-nav__submenu-chevron`, small: true, children: /* @__PURE__ */ jsx(ChevronDown, { size: 20 }) }) ] }), /* @__PURE__ */ jsx("ul", { className: `${prefix}--side-nav__menu`, children })] }); }); SideNavMenu.displayName = "SideNavMenu"; SideNavMenu.propTypes = { children: PropTypes.node, className: PropTypes.string, defaultExpanded: PropTypes.bool, isActive: PropTypes.bool, isSideNavExpanded: PropTypes.bool, large: PropTypes.bool, renderIcon: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), tabIndex: PropTypes.number, 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 (!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; }); if (React.isValidElement(children)) { const props = children.props; if (props.isActive === true || props["aria-current"]) return true; } return false; } //#endregion export { SideNavMenu as default };