CustomNav.jsx 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. import React, {
  2. useEffect, useState, useRef, useMemo, useCallback,
  3. } from 'react';
  4. import PropTypes from 'prop-types';
  5. import {
  6. Nav, NavItem, NavLink,
  7. } from 'reactstrap';
  8. import styles from './CustomNav.module.scss';
  9. function getBreakpointOneLevelLarger(breakpoint) {
  10. switch (breakpoint) {
  11. case 'sm':
  12. return 'md';
  13. case 'md':
  14. return 'lg';
  15. case 'lg':
  16. return 'xl';
  17. case 'xl':
  18. default:
  19. return 'xxl';
  20. }
  21. }
  22. export const CustomNavDropdown = (props) => {
  23. const {
  24. activeTab, navTabMapping, onNavSelected,
  25. } = props;
  26. const activeObj = navTabMapping[activeTab];
  27. const menuItemClickHandler = useCallback((key) => {
  28. if (onNavSelected != null) {
  29. onNavSelected(key);
  30. }
  31. }, [onNavSelected]);
  32. return (
  33. <div className="grw-custom-nav-dropdown btn-group btn-block">
  34. <button
  35. className="btn btn-outline-primary btn-lg btn-block dropdown-toggle text-right"
  36. type="button"
  37. data-toggle="dropdown"
  38. aria-haspopup="true"
  39. aria-expanded="false"
  40. >
  41. <span className="float-left">
  42. { activeObj != null && (
  43. <><activeObj.Icon /> {activeObj.i18n}</>
  44. ) }
  45. </span>
  46. </button>
  47. <div className="dropdown-menu dropdown-menu-right">
  48. {Object.entries(navTabMapping).map(([key, value]) => {
  49. const isActive = activeTab === key;
  50. const isLinkEnabled = value.isLinkEnabled != null ? value.isLinkEnabled(value) : true;
  51. const { Icon, i18n } = value;
  52. return (
  53. <button
  54. key={key}
  55. type="button"
  56. className={`dropdown-item px-3 py-2 ${isActive ? 'active' : ''}`}
  57. disabled={!isLinkEnabled}
  58. onClick={() => menuItemClickHandler(key)}
  59. >
  60. <Icon /> {i18n}
  61. </button>
  62. );
  63. })}
  64. </div>
  65. </div>
  66. );
  67. };
  68. CustomNavDropdown.propTypes = {
  69. navTabMapping: PropTypes.object.isRequired,
  70. activeTab: PropTypes.string,
  71. onNavSelected: PropTypes.func,
  72. };
  73. export const CustomNavTab = (props) => {
  74. const navContainer = useRef();
  75. const [sliderWidth, setSliderWidth] = useState(0);
  76. const [sliderMarginLeft, setSliderMarginLeft] = useState(0);
  77. const {
  78. activeTab, navTabMapping, onNavSelected, hideBorderBottom, breakpointToHideInactiveTabsDown, navRightElement,
  79. } = props;
  80. const navTabRefs = useMemo(() => {
  81. const obj = {};
  82. Object.keys(navTabMapping).forEach((key) => {
  83. obj[key] = React.createRef();
  84. });
  85. return obj;
  86. }, [navTabMapping]);
  87. const navLinkClickHandler = useCallback((key) => {
  88. if (onNavSelected != null) {
  89. onNavSelected(key);
  90. }
  91. }, [onNavSelected]);
  92. function registerNavLink(key, elm) {
  93. if (elm != null) {
  94. navTabRefs[key] = elm;
  95. }
  96. }
  97. // Might make this dynamic for px, %, pt, em
  98. function getPercentage(min, max) {
  99. return min / max * 100;
  100. }
  101. useEffect(() => {
  102. if (activeTab == null || activeTab === '') {
  103. return;
  104. }
  105. if (navContainer == null) {
  106. return;
  107. }
  108. let tempML = 0;
  109. const styles = Object.entries(navTabRefs).map((el) => {
  110. const width = getPercentage(el[1].offsetWidth, navContainer.current.offsetWidth);
  111. const marginLeft = tempML;
  112. tempML += width;
  113. return { width, marginLeft };
  114. });
  115. const { width, marginLeft } = styles[navTabMapping[activeTab].index];
  116. setSliderWidth(width);
  117. setSliderMarginLeft(marginLeft);
  118. }, [activeTab, navTabRefs, navTabMapping]);
  119. // determine inactive classes to hide NavItem
  120. const inactiveClassnames = [];
  121. if (breakpointToHideInactiveTabsDown != null) {
  122. const breakpointOneLevelLarger = getBreakpointOneLevelLarger(breakpointToHideInactiveTabsDown);
  123. inactiveClassnames.push('d-none');
  124. inactiveClassnames.push(`d-${breakpointOneLevelLarger}-block`);
  125. }
  126. return (
  127. <div className={`grw-custom-nav-tab ${styles['grw-custom-nav-tab']}`}>
  128. <div ref={navContainer} className="d-flex justify-content-between">
  129. <Nav className="nav-title">
  130. {Object.entries(navTabMapping).map(([key, value]) => {
  131. const isActive = activeTab === key;
  132. const isLinkEnabled = value.isLinkEnabled != null ? value.isLinkEnabled(value) : true;
  133. const { Icon, i18n } = value;
  134. return (
  135. <NavItem
  136. key={key}
  137. className={`p-0 ${isActive ? 'active' : inactiveClassnames.join(' ')}`}
  138. >
  139. <NavLink type="button" key={key} innerRef={elm => registerNavLink(key, elm)} disabled={!isLinkEnabled} onClick={() => navLinkClickHandler(key)}>
  140. <Icon /> {i18n}
  141. </NavLink>
  142. </NavItem>
  143. );
  144. })}
  145. </Nav>
  146. {navRightElement}
  147. </div>
  148. <hr className="my-0 grw-nav-slide-hr border-none" style={{ width: `${sliderWidth}%`, marginLeft: `${sliderMarginLeft}%` }} />
  149. { !hideBorderBottom && <hr className="my-0 border-top-0 border-bottom" /> }
  150. </div>
  151. );
  152. };
  153. CustomNavTab.propTypes = {
  154. activeTab: PropTypes.string.isRequired,
  155. navTabMapping: PropTypes.object.isRequired,
  156. onNavSelected: PropTypes.func,
  157. hideBorderBottom: PropTypes.bool,
  158. breakpointToHideInactiveTabsDown: PropTypes.oneOf(['xs', 'sm', 'md', 'lg', 'xl']),
  159. navRightElement: PropTypes.node,
  160. };
  161. CustomNavTab.defaultProps = {
  162. hideBorderBottom: false,
  163. };
  164. const CustomNav = (props) => {
  165. const tabClassnames = ['d-none'];
  166. const dropdownClassnames = ['d-block'];
  167. // determine classes to show/hide
  168. const breakpointOneLevelLarger = getBreakpointOneLevelLarger(props.breakpointToSwitchDropdownDown);
  169. tabClassnames.push(`d-${breakpointOneLevelLarger}-block`);
  170. dropdownClassnames.push(`d-${breakpointOneLevelLarger}-none`);
  171. return (
  172. <div className="grw-custom-nav">
  173. <div className={tabClassnames.join(' ')}>
  174. <CustomNavTab {...props} />
  175. </div>
  176. <div className={dropdownClassnames.join(' ')}>
  177. <CustomNavDropdown {...props} />
  178. </div>
  179. </div>
  180. );
  181. };
  182. CustomNav.propTypes = {
  183. activeTab: PropTypes.string.isRequired,
  184. navTabMapping: PropTypes.object.isRequired,
  185. onNavSelected: PropTypes.func,
  186. hideBorderBottom: PropTypes.bool,
  187. breakpointToHideInactiveTabsDown: PropTypes.oneOf(['xs', 'sm', 'md', 'lg', 'xl']),
  188. breakpointToSwitchDropdownDown: PropTypes.oneOf(['xs', 'sm', 'md', 'lg', 'xl']),
  189. };
  190. CustomNav.defaultProps = {
  191. hideBorderBottom: false,
  192. breakpointToSwitchDropdownDown: 'sm',
  193. };
  194. export default CustomNav;