UNPKG

@razorpay/blade

Version:

The Design System that powers Razorpay

436 lines (430 loc) 20.3 kB
import _slicedToArray from '@babel/runtime/helpers/slicedToArray'; import _objectWithoutProperties from '@babel/runtime/helpers/objectWithoutProperties'; import _defineProperty from '@babel/runtime/helpers/defineProperty'; import React__default from 'react'; import styled from 'styled-components'; import { SideNavContext } from './SideNavContext.js'; import { classes, COLLAPSED_L1_WIDTH, TRANSITION_CLEANUP_DELAY, HOVER_AGAIN_DELAY, SIDE_NAV_EXPANDED_L1_WIDTH_BASE, SIDE_NAV_EXPANDED_L1_WIDTH_XL, L1_EXIT_HOVER_DELAY, SKIP_NAV_ID } from './tokens.js'; import '../Box/BaseBox/index.js'; import '../../utils/index.js'; import '../Drawer/index.js'; import { SkipNavLink, SkipNavContent } from '../SkipNav/SkipNav.web.js'; import { useIsMobile } from '../../utils/useIsMobile.js'; import '../Box/styledProps/index.js'; import '../../utils/metaAttribute/index.js'; import '../../utils/makeAnalyticsAttribute/index.js'; import '../../tokens/global/index.js'; import { jsx, jsxs, Fragment } from 'react/jsx-runtime'; import { BaseBox } from '../Box/BaseBox/BaseBox.web.js'; import { makeMotionTime } from '../../utils/makeMotionTime/makeMotionTime.web.js'; import { makeSpace } from '../../utils/makeSpace/makeSpace.js'; import { makeSize } from '../../utils/makeSize/makeSize.js'; import { makeBorderSize } from '../../utils/makeBorderSize/makeBorderSize.js'; import { size } from '../../tokens/global/size.js'; import { Drawer } from '../Drawer/Drawer.web.js'; import { DrawerHeader, DrawerBody } from '../Drawer/DrawerSubcomponents.web.js'; import { metaAttribute } from '../../utils/metaAttribute/metaAttribute.web.js'; import { MetaConstants } from '../../utils/metaAttribute/metaConstants.js'; import { getStyledProps } from '../Box/styledProps/getStyledProps.js'; import { makeAnalyticsAttribute } from '../../utils/makeAnalyticsAttribute/makeAnalyticsAttribute.js'; var _excluded = ["children", "isOpen", "onDismiss", "onVisibleLevelChange", "banner", "testID"]; function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } function _createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it["return"] != null) it["return"](); } finally { if (didErr) throw err; } } }; } function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; } var COLLAPSED = classes.COLLAPSED, SHOW_WHEN_COLLAPSED = classes.SHOW_WHEN_COLLAPSED, HIDE_WHEN_COLLAPSED = classes.HIDE_WHEN_COLLAPSED, TRANSITIONING = classes.TRANSITIONING, L1_ITEM_WRAPPER = classes.L1_ITEM_WRAPPER; var MobileL1Container = /*#__PURE__*/styled(BaseBox).withConfig({ displayName: "SideNavweb__MobileL1Container", componentId: "sc-1obm5ij-0" })(function () { return _defineProperty({}, ".".concat(SHOW_WHEN_COLLAPSED), { display: 'none' }); }); var StyledL1Menu = /*#__PURE__*/styled(BaseBox).withConfig({ displayName: "SideNavweb__StyledL1Menu", componentId: "sc-1obm5ij-1" })(function (props) { var quick = makeMotionTime(props.theme.motion.duration.quick); var xmoderate = makeMotionTime(props.theme.motion.duration.xmoderate); var easing = props.theme.motion.easing; var l1Expand = "width ".concat(xmoderate, " ").concat(easing.entrance); var l1Collapse = "width ".concat(quick, " ").concat(easing.exit); return _defineProperty(_defineProperty(_defineProperty({ width: '100%', transition: l1Expand }, "& > .".concat(L1_ITEM_WRAPPER), { padding: makeSpace(props.theme.spacing[3]) }), ".".concat(SHOW_WHEN_COLLAPSED), { display: 'none' }), "&.".concat(COLLAPSED), _defineProperty(_defineProperty(_defineProperty({ width: makeSize(COLLAPSED_L1_WIDTH), transition: l1Collapse }, "& > .".concat(L1_ITEM_WRAPPER), { padding: "".concat(makeSpace(props.theme.spacing[3]), " ").concat(makeSpace(props.theme.spacing[3])) }), "&:not(.".concat(TRANSITIONING, ") .").concat(HIDE_WHEN_COLLAPSED), { display: 'none' }), "&:not(.".concat(TRANSITIONING, ") .").concat(SHOW_WHEN_COLLAPSED), { display: 'initial' })); }); var StyledL2PortalContainer = /*#__PURE__*/styled(BaseBox).withConfig({ displayName: "SideNavweb__StyledL2PortalContainer", componentId: "sc-1obm5ij-2" })(function () { return { // This ensures that the portal node has 100% height when it has items '& > div:not(:empty)': { height: '100%' } }; }); var getL1MenuClassName = function getL1MenuClassName(_ref3) { var isL1Collapsed = _ref3.isL1Collapsed, isL1Hovered = _ref3.isL1Hovered, isTransitioning = _ref3.isTransitioning; var isMenuCollapsed = isL1Collapsed && !isL1Hovered; if (isMenuCollapsed) { if (isTransitioning) { return "".concat(COLLAPSED, " ").concat(TRANSITIONING); } return COLLAPSED; } return ''; }; var BannerContainer = /*#__PURE__*/styled(BaseBox).withConfig({ displayName: "SideNavweb__BannerContainer", componentId: "sc-1obm5ij-3" })(function (props) { return { '&:not(:empty)': { borderBottom: makeBorderSize(props.theme.border.width.thin), borderBottomStyle: 'solid', borderBottomColor: props.theme.colors.surface.border.gray.muted, borderRight: makeBorderSize(props.theme.border.width.thin), borderRightStyle: 'solid', borderRightColor: props.theme.colors.surface.border.gray.muted, padding: makeSpace(props.theme.spacing[3]), maxHeight: makeSize(size['100']), width: '100%' } }; }); /** * ### SideNav component * * The side navigation is positioned along the left side of the screen that provides quick access to different sections or functionalities of the application. * * --- * * #### Usage * * SideNav requires handling active state with React Router, Checkout Usage with React Router v6 at - [SideNav Documentation](https://blade.razorpay.com/?path=/docs/components-sidenav--docs) * */ var _SideNav = function _SideNav(_ref4, ref) { var children = _ref4.children, isOpen = _ref4.isOpen, onDismiss = _ref4.onDismiss, onVisibleLevelChange = _ref4.onVisibleLevelChange, banner = _ref4.banner, testID = _ref4.testID, rest = _objectWithoutProperties(_ref4, _excluded); var l2PortalContainerRef = React__default.useRef(null); var l1ContainerRef = React__default.useRef(null); var timeoutIdsRef = React__default.useRef([]); var mouseOverTimeoutRef = React__default.useRef(); var _React$useState = React__default.useState(false), _React$useState2 = _slicedToArray(_React$useState, 2), isL1Collapsed = _React$useState2[0], setIsL1Collapsed = _React$useState2[1]; var _React$useState3 = React__default.useState(false), _React$useState4 = _slicedToArray(_React$useState3, 2), isMobileL2Open = _React$useState4[0], setIsMobileL2Open = _React$useState4[1]; var _React$useState5 = React__default.useState(false), _React$useState6 = _slicedToArray(_React$useState5, 2), isL1Hovered = _React$useState6[0], setIsL1Hovered = _React$useState6[1]; var _React$useState7 = React__default.useState(true), _React$useState8 = _slicedToArray(_React$useState7, 2), isHoverAgainEnabled = _React$useState8[0], setIsHoverAgainEnabled = _React$useState8[1]; var _React$useState9 = React__default.useState(false), _React$useState10 = _slicedToArray(_React$useState9, 2), isTransitioning = _React$useState10[0], setIsTransitioning = _React$useState10[1]; var _React$useState11 = React__default.useState(''), _React$useState12 = _slicedToArray(_React$useState11, 2), l2DrawerTitle = _React$useState12[0], setL2DrawerTitle = _React$useState12[1]; var isMobile = useIsMobile(); var closeMobileNav = function closeMobileNav() { if (isMobile) { setIsMobileL2Open(false); onDismiss === null || onDismiss === void 0 ? void 0 : onDismiss(); onVisibleLevelChange === null || onVisibleLevelChange === void 0 ? void 0 : onVisibleLevelChange({ visibleLevel: 0 }); } }; var cleanupTransition = function cleanupTransition() { var clearTransitionTimeout = setTimeout(function () { if (isTransitioning) { setIsTransitioning(false); } }, TRANSITION_CLEANUP_DELAY); timeoutIdsRef.current.push(clearTransitionTimeout); }; var collapseL1 = function collapseL1(title) { if (isMobile) { setL2DrawerTitle(title); setIsMobileL2Open(true); onVisibleLevelChange === null || onVisibleLevelChange === void 0 ? void 0 : onVisibleLevelChange({ visibleLevel: 2 }); return; } if (!isL1Collapsed) { setIsL1Collapsed(true); onVisibleLevelChange === null || onVisibleLevelChange === void 0 ? void 0 : onVisibleLevelChange({ visibleLevel: 2 }); } }; var expandL1 = function expandL1() { if (isMobile) { setIsMobileL2Open(false); onVisibleLevelChange === null || onVisibleLevelChange === void 0 ? void 0 : onVisibleLevelChange({ visibleLevel: 1 }); return; } // Ensures that if Normal L1 item is clicked, the L1 stays expanded if (isL1Collapsed) { setIsL1Collapsed(false); // We want to avoid calling onVisibleLevelChange twice when L1 is hovered and then item on L1 is selected if (!isL1Hovered) { onVisibleLevelChange === null || onVisibleLevelChange === void 0 ? void 0 : onVisibleLevelChange({ visibleLevel: 1 }); } } }; /** * Handles L1 -> L2 menu changes based on active item */ var onLinkActiveChange = function onLinkActiveChange(args) { var isL1ItemActive = args.level === 1 && args.isActive; if (isL1ItemActive) { if (args.isL2Trigger) { // Click on L2 Trigger collapseL1(args.title); // `args.isFirstRender` checks if the item that triggered this change, triggered it during first render or during subsequent change if (!args.isFirstRender) { setIsTransitioning(true); cleanupTransition(); setIsL1Hovered(false); setIsHoverAgainEnabled(false); // For some delay, we disable hover to expand behaviour to avoid buggy flicker when cursor is on L1 while its trying to close var hoverAgainTimeout = setTimeout(function () { setIsHoverAgainEnabled(true); }, HOVER_AGAIN_DELAY); timeoutIdsRef.current.push(hoverAgainTimeout); } } else { // Click on normal L1 Item expandL1(); } } }; var contextValue = React__default.useMemo(function () { return { l2PortalContainerRef: l2PortalContainerRef, onLinkActiveChange: onLinkActiveChange, closeMobileNav: closeMobileNav, isL1Collapsed: isMobile ? isMobileL2Open : isL1Collapsed, setIsL1Collapsed: setIsL1Collapsed, isL1Hovered: isL1Hovered }; }, // eslint-disable-next-line react-hooks/exhaustive-deps [isL1Collapsed, isMobile, isMobileL2Open, isL1Hovered]); React__default.useEffect(function () { return function () { var _iterator = _createForOfIteratorHelper(timeoutIdsRef.current), _step; try { for (_iterator.s(); !(_step = _iterator.n()).done;) { var timeoutId = _step.value; clearTimeout(timeoutId); } } catch (err) { _iterator.e(err); } finally { _iterator.f(); } timeoutIdsRef.current = []; }; }, []); return /*#__PURE__*/jsx(SideNavContext.Provider, { value: contextValue, children: isMobile && onDismiss ? /*#__PURE__*/jsxs(Fragment, { children: [/*#__PURE__*/jsxs(Drawer, { isOpen: isOpen !== null && isOpen !== void 0 ? isOpen : false, onDismiss: closeMobileNav, children: [/*#__PURE__*/jsx(DrawerHeader, { title: "Main Menu" }), /*#__PURE__*/jsx(DrawerBody, { children: /*#__PURE__*/jsx(MobileL1Container, _objectSpread(_objectSpread({ display: "flex", flexDirection: "column", justifyContent: "space-between", className: "mobile-l1-container", height: "100%" }, metaAttribute({ name: MetaConstants.SideNav, testID: testID })), {}, { children: children })) })] }), /*#__PURE__*/jsxs(Drawer, { isOpen: isMobileL2Open, onDismiss: function onDismiss() { return expandL1(); }, isLazy: false, children: [/*#__PURE__*/jsx(DrawerHeader, { title: l2DrawerTitle }), /*#__PURE__*/jsx(DrawerBody, { children: /*#__PURE__*/jsx(BaseBox, { ref: l2PortalContainerRef }) })] })] }) : /*#__PURE__*/jsxs(BaseBox, _objectSpread(_objectSpread(_objectSpread(_objectSpread({ ref: ref, position: "fixed", backgroundColor: "surface.background.gray.moderate", height: "100%", top: "spacing.0", left: "spacing.0", display: { base: 'none', m: 'flex' }, flexDirection: "column", width: { base: makeSize(SIDE_NAV_EXPANDED_L1_WIDTH_BASE), xl: makeSize(SIDE_NAV_EXPANDED_L1_WIDTH_XL) }, as: "nav" }, metaAttribute({ name: MetaConstants.SideNav, testID: testID })), getStyledProps(rest)), makeAnalyticsAttribute(rest)), {}, { children: [banner ? /*#__PURE__*/jsx(BannerContainer, { children: banner }) : null, /*#__PURE__*/jsxs(BaseBox, { position: "relative", display: "block", flex: "1", width: "100%", children: [/*#__PURE__*/jsx(StyledL2PortalContainer, { position: "absolute", backgroundColor: "surface.background.gray.moderate", height: "100%", width: "100%", top: "spacing.0", left: "spacing.0", id: "blade-sidenav-l2", borderRightWidth: "thin", borderRightColor: "surface.border.gray.muted", ref: l2PortalContainerRef }), /*#__PURE__*/jsxs(StyledL1Menu, { ref: l1ContainerRef, id: "blade-sidenav-l1", className: getL1MenuClassName({ isL1Collapsed: isL1Collapsed, isL1Hovered: isL1Hovered, isTransitioning: isTransitioning }), position: "absolute", display: "flex", flexDirection: "column", justifyContent: "space-between", backgroundColor: "surface.background.gray.moderate", height: "100%", overflow: "hidden", top: "spacing.0", left: "spacing.0", borderRightWidth: "thin", borderRightColor: "surface.border.gray.muted", onTransitionEnd: function onTransitionEnd(e) { // This check ensures transitioning is set to false only when its true // And only the l1Container element's transitions are considered and other transitions of l1 expand or child elements are ignored if (isTransitioning && l1ContainerRef.current === e.target) { setIsTransitioning(false); } } // Hmm you might be wondering, why is `onMouseOver` paired with `onMouseLeave`? A sane person would pair `onMouseOver` with `onMouseOut`, and `onMouseEnter` with `onMouseLeave` // since they are logical equivalents of each other. So why don't we do that? Hold tight, you're in for a ride ☕️. // // 1. In an ideal scenario, we would put `onMouseEnter` and `onMouseLeave` here and expect things to work. // 2. The L2 menu of our SideNav is React Portalled out of the L1 child // 3. React considers its own children as true children for JS events and not DOM children (Checkout React Portal Caveats - https://react.dev/reference/react-dom/createPortal#caveats) // 3. In the next ideal scenario, we would put `e.stopPropagation` on child component of portal like React recommends, except mouseenter, mouseleave events don't propagate at all (https://developer.mozilla.org/en-US/docs/Web/API/Element/mouseenter_event#usage_notes) // 4. So `onMouseEnter` gets triggered on L2 enter. But we don't want to open L1 menu on L2 hover // 5. Thus we use `onMouseOver` for hover part and call e.stopPropagation in portal child (SideNavLevel). // 6. But in case of unhover/leave, we don't want to trigger mouseOut for all child components individually. We want 1 hover out of L1 menu. Thus we use `onMouseLeave` , onMouseOver: function onMouseOver() { if (mouseOverTimeoutRef.current) { clearTimeout(mouseOverTimeoutRef.current); } if (isL1Collapsed && isHoverAgainEnabled && !isL1Hovered) { setIsL1Hovered(true); onVisibleLevelChange === null || onVisibleLevelChange === void 0 ? void 0 : onVisibleLevelChange({ visibleLevel: 1 }); } }, onMouseLeave: function onMouseLeave() { if (isL1Collapsed && isL1Hovered) { mouseOverTimeoutRef.current = setTimeout(function () { setIsL1Hovered(false); setIsTransitioning(true); cleanupTransition(); onVisibleLevelChange === null || onVisibleLevelChange === void 0 ? void 0 : onVisibleLevelChange({ visibleLevel: 2 }); }, L1_EXIT_HOVER_DELAY); } // If L1 is collapsed and not hovered we want to change visible level to 2 // This state/edgecase happens when user clicks on a nested nav and it collapses the L1 causing isL1Hovered to be false if (isL1Collapsed && !isL1Hovered) { onVisibleLevelChange === null || onVisibleLevelChange === void 0 ? void 0 : onVisibleLevelChange({ visibleLevel: 2 }); } }, children: [/*#__PURE__*/jsx(SkipNavLink, { id: SKIP_NAV_ID, _hasBackground: true }), children] }), /*#__PURE__*/jsx(SkipNavContent, { id: SKIP_NAV_ID })] })] })) }); }; var SideNav = /*#__PURE__*/React__default.forwardRef(_SideNav); export { SideNav }; //# sourceMappingURL=SideNav.web.js.map