UNPKG

@razorpay/blade

Version:

The Design System that powers Razorpay

526 lines (519 loc) 24.2 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, SIDE_NAV_EXPANDED_L1_WIDTH_BASE, SIDE_NAV_EXPANDED_L1_WIDTH_XL, TRANSITION_CLEANUP_DELAY, HOVER_AGAIN_DELAY, L1_EXIT_HOVER_DELAY, SKIP_NAV_ID } from './tokens.js'; import '../../tokens/global/index.js'; import '../../utils/index.js'; import '../../utils/makeAnalyticsAttribute/index.js'; import '../../utils/metaAttribute/index.js'; import { useIsMobile } from '../../utils/useIsMobile.js'; import '../Box/BaseBox/index.js'; import '../Box/styledProps/index.js'; import '../Drawer/index.js'; import { SkipNavLink, SkipNavContent } from '../SkipNav/SkipNav.web.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 { makeSize } from '../../utils/makeSize/makeSize.js'; import { makeSpace } from '../../utils/makeSpace/makeSpace.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", "onExpandChange", "onExpandTransitionEnd", "banner", "backgroundColor", "testID", "isExpanded"]; function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, 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 o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t["return"] || t["return"](); } finally { if (u) throw o; } } }; } function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } } function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; } 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; } 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); var textVisibilityProps = props.$isSideNavExpandable ? {} : _defineProperty(_defineProperty({}, ".".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' })); return _objectSpread(_defineProperty({ width: '100%', // the overall sidenav expands and collapses in this case. No need transition here transition: props.$isSideNavExpandable ? 'none' : l1Expand }, "& > .".concat(L1_ITEM_WRAPPER), { padding: makeSpace(props.theme.spacing[3]) }), textVisibilityProps); }); var StyledSideNavContainer = /*#__PURE__*/styled(BaseBox).withConfig({ displayName: "SideNavweb__StyledSideNavContainer", componentId: "sc-1obm5ij-2" })(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 sideNavExpand = "width ".concat(xmoderate, " ").concat(easing.entrance); var sideNavCollapse = "width ".concat(quick, " ").concat(easing.exit); if (!props.$isSideNavExpandable) { return {}; } return _defineProperty(_defineProperty(_defineProperty({ transition: sideNavExpand }, "&.".concat(COLLAPSED), _defineProperty(_defineProperty({ width: makeSize(COLLAPSED_L1_WIDTH), transition: sideNavCollapse }, "&:not(.".concat(TRANSITIONING, ") .").concat(HIDE_WHEN_COLLAPSED), { opacity: '0', pointerEvents: 'none', transition: "opacity ".concat(quick, " ").concat(easing.exit) }), "&:not(.".concat(TRANSITIONING, ") .").concat(SHOW_WHEN_COLLAPSED), { opacity: '1', pointerEvents: 'auto', transition: "opacity ".concat(quick, " ").concat(easing.entrance) })), ".".concat(SHOW_WHEN_COLLAPSED), { opacity: '0', pointerEvents: 'none', transition: "opacity ".concat(quick, " ").concat(easing.exit) }), ".".concat(HIDE_WHEN_COLLAPSED), { opacity: '1', pointerEvents: 'auto', transition: "opacity ".concat(quick, " ").concat(easing.entrance) }); }); var StyledL2PortalContainer = /*#__PURE__*/styled(BaseBox).withConfig({ displayName: "SideNavweb__StyledL2PortalContainer", componentId: "sc-1obm5ij-3" })(function () { return { // This ensures that the portal node has 100% height when it has items '& > div:not(:empty)': { height: '100%' } }; }); var getL1MenuClassName = function getL1MenuClassName(_ref4) { var isL1Collapsed = _ref4.isL1Collapsed, isL1Hovered = _ref4.isL1Hovered, isTransitioning = _ref4.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-4" })(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(_ref5, ref) { var children = _ref5.children, isOpen = _ref5.isOpen, onDismiss = _ref5.onDismiss, onVisibleLevelChange = _ref5.onVisibleLevelChange, onExpandChange = _ref5.onExpandChange, onExpandTransitionEnd = _ref5.onExpandTransitionEnd, banner = _ref5.banner, _ref5$backgroundColor = _ref5.backgroundColor, backgroundColor = _ref5$backgroundColor === void 0 ? 'surface.background.gray.moderate' : _ref5$backgroundColor, testID = _ref5.testID, _isExpanded = _ref5.isExpanded, rest = _objectWithoutProperties(_ref5, _excluded); var l2PortalContainerRef = React__default.useRef(null); var l1ContainerRef = React__default.useRef(null); var timeoutIdsRef = React__default.useRef([]); var mouseOverTimeoutRef = React__default.useRef(); var prevIsSideNavCollapsedRef = 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$useState0 = _slicedToArray(_React$useState9, 2), isTransitioning = _React$useState0[0], setIsTransitioning = _React$useState0[1]; var _React$useState1 = React__default.useState(''), _React$useState10 = _slicedToArray(_React$useState1, 2), l2DrawerTitle = _React$useState10[0], setL2DrawerTitle = _React$useState10[1]; var isMobile = useIsMobile(); var isSideNavCollapsed = _isExpanded === false; var effectiveIsL1Collapsed = isMobile ? isMobileL2Open : isSideNavCollapsed || isL1Collapsed; var effectiveIsL1Hovered = isSideNavCollapsed ? false : isL1Hovered; var sideNavWidth = isSideNavCollapsed ? makeSize(COLLAPSED_L1_WIDTH) : { base: makeSize(SIDE_NAV_EXPANDED_L1_WIDTH_BASE), xl: makeSize(SIDE_NAV_EXPANDED_L1_WIDTH_XL) }; var closeMobileNav = function closeMobileNav() { if (isMobile) { setIsMobileL2Open(false); onDismiss === null || onDismiss === void 0 || onDismiss(); onVisibleLevelChange === null || onVisibleLevelChange === void 0 || onVisibleLevelChange({ visibleLevel: 0 }); } }; var cleanupTransition = React__default.useCallback(function () { var clearTransitionTimeout = setTimeout(function () { setIsTransitioning(function (isCurrentlyTransitioning) { return isCurrentlyTransitioning ? false : isCurrentlyTransitioning; }); }, TRANSITION_CLEANUP_DELAY); timeoutIdsRef.current.push(clearTransitionTimeout); }, []); var startL1Transition = React__default.useCallback(function () { setIsTransitioning(true); cleanupTransition(); }, [cleanupTransition]); var collapseL1 = function collapseL1(title) { if (isMobile) { setL2DrawerTitle(title); setIsMobileL2Open(true); onVisibleLevelChange === null || onVisibleLevelChange === void 0 || onVisibleLevelChange({ visibleLevel: 2 }); return; } if (!isL1Collapsed) { setIsL1Collapsed(true); onVisibleLevelChange === null || onVisibleLevelChange === void 0 || onVisibleLevelChange({ visibleLevel: 2 }); } }; var expandL1 = function expandL1() { if (isMobile) { setIsMobileL2Open(false); onVisibleLevelChange === null || onVisibleLevelChange === void 0 || onVisibleLevelChange({ visibleLevel: 1 }); return; } if (isSideNavCollapsed) { 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 || 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) { startL1Transition(); 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: effectiveIsL1Collapsed, setIsL1Collapsed: setIsL1Collapsed, isL1Hovered: effectiveIsL1Hovered, isSideNavCollapsed: isSideNavCollapsed }; }, // eslint-disable-next-line react-hooks/exhaustive-deps [effectiveIsL1Collapsed, effectiveIsL1Hovered, isSideNavCollapsed, isMobile]); 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 = []; }; }, []); React__default.useEffect(function () { if (!isMobile && isSideNavCollapsed && isL1Hovered) { setIsL1Hovered(false); } }, [isMobile, isSideNavCollapsed, isL1Hovered]); React__default.useEffect(function () { var prevIsSideNavCollapsed = prevIsSideNavCollapsedRef.current; prevIsSideNavCollapsedRef.current = isSideNavCollapsed; if (isMobile || prevIsSideNavCollapsed === undefined) { return; } if (prevIsSideNavCollapsed !== isSideNavCollapsed) { startL1Transition(); onExpandChange === null || onExpandChange === void 0 || onExpandChange({ isExpanded: !isSideNavCollapsed }); } }, [isMobile, isSideNavCollapsed, onExpandChange, startL1Transition]); 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(StyledSideNavContainer, _objectSpread(_objectSpread(_objectSpread(_objectSpread({ $isSideNavExpandable: typeof _isExpanded !== 'undefined', ref: ref, className: isSideNavCollapsed ? COLLAPSED : '', position: "fixed", backgroundColor: backgroundColor, height: "100%", top: "spacing.0", left: "spacing.0", display: { base: 'none', m: 'flex' }, flexDirection: "column", width: sideNavWidth, as: "nav" }, metaAttribute({ name: MetaConstants.SideNav, testID: testID })), getStyledProps(rest)), makeAnalyticsAttribute(rest)), {}, { onTransitionEnd: function onTransitionEnd(e) { if (e.target !== e.currentTarget || e.propertyName !== 'width' || isMobile) { return; } onExpandTransitionEnd === null || onExpandTransitionEnd === void 0 || onExpandTransitionEnd({ isExpanded: !isSideNavCollapsed }); }, 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", display: isSideNavCollapsed ? 'none' : 'block', backgroundColor: backgroundColor, 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: effectiveIsL1Collapsed, isL1Hovered: effectiveIsL1Hovered, isTransitioning: isTransitioning }), $isSideNavExpandable: typeof _isExpanded !== 'undefined', position: "absolute", display: "flex", flexDirection: "column", justifyContent: "space-between", backgroundColor: backgroundColor, 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 (!isMobile && isSideNavCollapsed) { return; } if (mouseOverTimeoutRef.current) { clearTimeout(mouseOverTimeoutRef.current); } if (isL1Collapsed && isHoverAgainEnabled && !isL1Hovered) { setIsL1Hovered(true); onVisibleLevelChange === null || onVisibleLevelChange === void 0 || onVisibleLevelChange({ visibleLevel: 1 }); } }, onMouseLeave: function onMouseLeave() { if (!isMobile && isSideNavCollapsed) { return; } if (isL1Collapsed && isL1Hovered) { mouseOverTimeoutRef.current = setTimeout(function () { setIsL1Hovered(false); startL1Transition(); onVisibleLevelChange === null || onVisibleLevelChange === 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 || 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