UNPKG

@carbon/react

Version:

React components for the Carbon Design System

258 lines (248 loc) 8.82 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 { extends as _extends } from '../../_virtual/_rollupPluginBabelHelpers.js'; import React, { useRef, isValidElement, createContext } from 'react'; import cx from 'classnames'; import PropTypes from 'prop-types'; import { AriaLabelPropType } from '../../prop-types/AriaPropTypes.js'; import { CARBON_SIDENAV_ITEMS } from './_utils.js'; import { usePrefix } from '../../internal/usePrefix.js'; import { Tab, Escape } 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 { breakpoints } from '@carbon/layout'; import { useMatchMedia } from '../../internal/useMatchMedia.js'; // TO-DO: comment back in when footer is added for rails // import SideNavFooter from './SideNavFooter'; const SideNavContext = /*#__PURE__*/createContext({}); function SideNavRenderFunction({ expanded: expandedProp, defaultExpanded = false, isChildOfHeader = true, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, children, onToggle, className: customClassName, // TO-DO: comment back in when footer is added for rails // translateById: t = (id) => translations[id], href, isFixedNav = false, isRail, isPersistent = true, addFocusListeners = true, addMouseListeners = true, onOverlayClick, onSideNavBlur, enterDelayMs = 100, ...other }, ref) { const prefix = usePrefix(); const { current: controlled } = useRef(expandedProp !== undefined); 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 }; // TO-DO: comment back in when footer is added for rails // const assistiveText = expanded // ? t('carbon.sidenav.state.open') // : t('carbon.sidenav.state.closed'); const className = cx(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 = cx({ [`${prefix}--side-nav__overlay`]: true, [`${prefix}--side-nav__overlay-active`]: expanded || expandedViaHoverState }); let childrenToRender = children; // Pass the expansion state as a prop, so children can update themselves to match childrenToRender = React.Children.map(children, child => { // if we are controlled, check for if we have hovered over or the expanded state, else just use the expanded state (uncontrolled) const currentExpansionState = controlled ? expandedViaHoverState || expanded : expanded; if (/*#__PURE__*/isValidElement(child)) { const childJsxElement = child; // avoid spreading `isSideNavExpanded` to non-Carbon UI Shell children return /*#__PURE__*/React.cloneElement(childJsxElement, { ...(CARBON_SIDENAV_ITEMS.includes(childJsxElement.type?.displayName ?? childJsxElement.type?.name) ? { isSideNavExpanded: currentExpansionState } : {}) }); } return child; }); 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 = () => { //if delay is enabled, and user intentionally clicks it to see it expanded immediately 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 lgMediaQuery = `(min-width: ${breakpoints.lg.width})`; const isLg = useMatchMedia(lgMediaQuery); return /*#__PURE__*/React.createElement(SideNavContext.Provider, { value: { isRail } }, isFixedNav ? null : /*#__PURE__*/ // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions React.createElement("div", { className: overlayClassName, onClick: onOverlayClick }), /*#__PURE__*/React.createElement("nav", _extends({ tabIndex: -1, ref: navRef, className: `${prefix}--side-nav__navigation ${className}`, inert: !isRail ? !(expanded || isLg) : undefined }, accessibilityLabel, eventHandlers, other), childrenToRender)); } const SideNav = /*#__PURE__*/React.forwardRef(SideNavRenderFunction); SideNav.displayName = 'SideNav'; SideNav.propTypes = { /** * Required props for accessibility label on the underlying menu */ ...AriaLabelPropType, /** * Specify whether focus and blur listeners are added. They are by default. */ addFocusListeners: PropTypes.bool, /** * Specify whether mouse entry/exit listeners are added. They are by default. */ addMouseListeners: PropTypes.bool, /** * Optionally provide a custom class to apply to the underlying `<li>` node */ className: PropTypes.string, /** * If `true`, the SideNav will be open on initial render. */ defaultExpanded: PropTypes.bool, /** * Specify the duration in milliseconds to delay before displaying the side navigation */ enterDelayMs: PropTypes.number, /** * If `true`, the SideNav will be expanded, otherwise it will be collapsed. * Using this prop causes SideNav to become a controlled component. */ expanded: PropTypes.bool, /** * Provide the `href` to the id of the element on your package that is the * main content. */ href: PropTypes.string, /** * Optionally provide a custom class to apply to the underlying `<li>` node */ isChildOfHeader: PropTypes.bool, /** * Specify if sideNav is standalone */ isFixedNav: PropTypes.bool, /** * Specify if the sideNav will be persistent above the lg breakpoint */ isPersistent: PropTypes.bool, /** * Optional prop to display the side nav rail. */ isRail: PropTypes.bool, /** * An optional listener that is called when the SideNav overlay is clicked * * @param {object} event */ onOverlayClick: PropTypes.func, /** * An optional listener that is called a callback to collapse the SideNav */ onSideNavBlur: PropTypes.func, /** * An optional listener that is called when an event that would cause * toggling the SideNav occurs. * * @param {object} event * @param {boolean} value */ onToggle: PropTypes.func /** * Provide a custom function for translating all message ids within this * component. This function will take in two arguments: the message Id and the * state of the component. From this, you should return a string representing * the label you want displayed or read by screen readers. */ // translateById: PropTypes.func, }; export { SideNavContext, SideNav as default };