UNPKG

@carbon/react

Version:

React components for the Carbon Design System

142 lines (140 loc) 5.57 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, Tab } from "../../internal/keyboard/keys.js"; import { match } from "../../internal/keyboard/match.js"; import { useMergedRefs } from "../../internal/useMergedRefs.js"; import { useWindowEvent } from "../../internal/useEvent.js"; import { useDelayedState } from "../../internal/useDelayedState.js"; import { useMatchMedia } from "../../internal/useMatchMedia.js"; import { AriaLabelPropType } from "../../prop-types/AriaPropTypes.js"; import { SideNavContextProvider } from "./SideNavContext.js"; import classNames from "classnames"; import { forwardRef, useEffect, useRef } from "react"; import PropTypes from "prop-types"; import { jsx, jsxs } from "react/jsx-runtime"; import { breakpoints } from "@carbon/layout"; //#region src/components/UIShell/SideNav.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 SideNav = forwardRef((props, ref) => { const { expanded: expandedProp, defaultExpanded = false, isChildOfHeader = true, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, children, onToggle, className: customClassName, href, isFixedNav = false, isRail, isPersistent = true, addFocusListeners = true, addMouseListeners = true, onOverlayClick, onSideNavBlur, enterDelayMs = 100, ...other } = props; const prefix = usePrefix(); const { current: controlled } = useRef(expandedProp !== void 0); const [expandedState, setExpandedState] = useDelayedState(defaultExpanded); const [expandedViaHoverState, setExpandedViaHoverState] = useDelayedState(defaultExpanded); const expanded = controlled ? expandedProp : expandedState; const sideNavRef = useRef(null); const navRef = useMergedRefs([sideNavRef, ref]); const handleToggle = (event, value = !expanded) => { if (!controlled) setExpandedState(value, enterDelayMs); if (onToggle) onToggle(event, value); if (controlled || isRail) setExpandedViaHoverState(value, enterDelayMs); }; const accessibilityLabel = { "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy }; const className = classNames(customClassName, { [`${prefix}--side-nav`]: true, [`${prefix}--side-nav--expanded`]: expanded || expandedViaHoverState, [`${prefix}--side-nav--collapsed`]: !expanded && isFixedNav, [`${prefix}--side-nav--rail`]: isRail, [`${prefix}--side-nav--ux`]: isChildOfHeader, [`${prefix}--side-nav--hidden`]: !isPersistent }); const overlayClassName = classNames({ [`${prefix}--side-nav__overlay`]: true, [`${prefix}--side-nav__overlay-active`]: expanded || expandedViaHoverState }); const currentExpansionState = controlled ? expandedViaHoverState || expanded : expanded; const eventHandlers = {}; if (addFocusListeners) { eventHandlers.onFocus = (event) => { if (!event.currentTarget.contains(event.relatedTarget) && isRail) handleToggle(event, true); }; eventHandlers.onBlur = (event) => { if (!event.currentTarget.contains(event.relatedTarget)) handleToggle(event, false); if (!event.currentTarget.contains(event.relatedTarget) && expanded && !isFixedNav) { if (onSideNavBlur) onSideNavBlur(); } }; eventHandlers.onKeyDown = (event) => { if (match(event, Escape)) { handleToggle(event, false); if (href) window.location.href = href; } }; } if (addMouseListeners && isRail) { eventHandlers.onMouseEnter = () => { handleToggle(true, true); }; eventHandlers.onMouseLeave = () => { setExpandedState(false); setExpandedViaHoverState(false); handleToggle(false, false); }; eventHandlers.onClick = () => { setExpandedState(true); setExpandedViaHoverState(true); handleToggle(true, true); }; } useWindowEvent("keydown", (event) => { const focusedElement = document.activeElement; if (match(event, Tab) && expanded && !isFixedNav && sideNavRef.current && focusedElement?.classList.contains(`${prefix}--header__menu-toggle`) && !focusedElement.closest("nav")) sideNavRef.current.focus(); }); const isLg = useMatchMedia(`(min-width: ${breakpoints.lg.width})`); const inertEnabled = !isRail ? !(expanded || isLg) : false; useEffect(() => { const node = sideNavRef.current; if (!node) return; if (inertEnabled) node.setAttribute("inert", ""); else node.removeAttribute("inert"); }, [inertEnabled]); return /* @__PURE__ */ jsxs(SideNavContextProvider, { isRail, isSideNavExpanded: currentExpansionState, children: [isFixedNav ? null : /* @__PURE__ */ jsx("div", { className: overlayClassName, onClick: onOverlayClick }), /* @__PURE__ */ jsx("nav", { tabIndex: -1, ref: navRef, className: `${prefix}--side-nav__navigation ${className}`, ...accessibilityLabel, ...eventHandlers, ...other, children })] }); }); SideNav.displayName = "SideNav"; SideNav.propTypes = { ...AriaLabelPropType, addFocusListeners: PropTypes.bool, addMouseListeners: PropTypes.bool, className: PropTypes.string, defaultExpanded: PropTypes.bool, enterDelayMs: PropTypes.number, expanded: PropTypes.bool, href: PropTypes.string, isChildOfHeader: PropTypes.bool, isFixedNav: PropTypes.bool, isPersistent: PropTypes.bool, isRail: PropTypes.bool, onOverlayClick: PropTypes.func, onSideNavBlur: PropTypes.func, onToggle: PropTypes.func }; //#endregion export { SideNav as default };