2
0

CustomNav.jsx 6.3 KB

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