UNPKG

@carbon/react

Version:

React components for the Carbon Design System

273 lines (261 loc) 9.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. */ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var _rollupPluginBabelHelpers = require('../../_virtual/_rollupPluginBabelHelpers.js'); var React = require('react'); var cx = require('classnames'); var PropTypes = require('prop-types'); var AriaPropTypes = require('../../prop-types/AriaPropTypes.js'); var _utils = require('./_utils.js'); var usePrefix = require('../../internal/usePrefix.js'); var keys = require('../../internal/keyboard/keys.js'); var match = require('../../internal/keyboard/match.js'); var useMergedRefs = require('../../internal/useMergedRefs.js'); var useEvent = require('../../internal/useEvent.js'); var useDelayedState = require('../../internal/useDelayedState.js'); var layout = require('@carbon/layout'); var useMatchMedia = require('../../internal/useMatchMedia.js'); // TO-DO: comment back in when footer is added for rails // import SideNavFooter from './SideNavFooter'; const SideNavContext = /*#__PURE__*/React.createContext({}); const frFn = React.forwardRef; const SideNav = frFn((props, ref) => { const { 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 } = props; const prefix = usePrefix.usePrefix(); const { current: controlled } = React.useRef(expandedProp !== undefined); const [expandedState, setExpandedState] = useDelayedState.useDelayedState(defaultExpanded); const [expandedViaHoverState, setExpandedViaHoverState] = useDelayedState.useDelayedState(defaultExpanded); const expanded = controlled ? expandedProp : expandedState; const sideNavRef = React.useRef(null); const navRef = useMergedRefs.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__*/React.isValidElement(child)) { const childJsxElement = child; // avoid spreading `isSideNavExpanded` to non-Carbon UI Shell children return /*#__PURE__*/React.cloneElement(childJsxElement, { ...(_utils.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.match(event, keys.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); }; } useEvent.useWindowEvent('keydown', event => { const focusedElement = document.activeElement; if (match.match(event, keys.Tab) && expanded && !isFixedNav && sideNavRef.current && focusedElement?.classList.contains(`${prefix}--header__menu-toggle`) && !focusedElement.closest('nav')) { sideNavRef.current.focus(); } }); const lgMediaQuery = `(min-width: ${layout.breakpoints.lg.width})`; const isLg = useMatchMedia.useMatchMedia(lgMediaQuery); const inertEnabled = !isRail ? !(expanded || isLg) : false; React.useEffect(() => { const node = sideNavRef.current; if (!node) return; if (inertEnabled) { node.setAttribute('inert', ''); } else { node.removeAttribute('inert'); } }, [inertEnabled]); 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", _rollupPluginBabelHelpers.extends({ tabIndex: -1, ref: navRef, className: `${prefix}--side-nav__navigation ${className}` }, accessibilityLabel, eventHandlers, other), childrenToRender)); }); SideNav.displayName = 'SideNav'; SideNav.propTypes = { /** * Required props for accessibility label on the underlying menu */ ...AriaPropTypes.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, }; exports.SideNavContext = SideNavContext; exports.default = SideNav;