@razorpay/blade
Version:
The Design System that powers Razorpay
436 lines (430 loc) • 20.3 kB
JavaScript
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