|
@@ -2,16 +2,20 @@ import React, {
|
|
|
useEffect, useState, useRef, useMemo, useCallback,
|
|
useEffect, useState, useRef, useMemo, useCallback,
|
|
|
} from 'react';
|
|
} from 'react';
|
|
|
|
|
|
|
|
-import PropTypes from 'prop-types';
|
|
|
|
|
|
|
+import { Breakpoint } from '@growi/ui/dist/interfaces/breakpoints';
|
|
|
import {
|
|
import {
|
|
|
Nav, NavItem, NavLink,
|
|
Nav, NavItem, NavLink,
|
|
|
} from 'reactstrap';
|
|
} from 'reactstrap';
|
|
|
|
|
|
|
|
|
|
+import { ICustomNavTabMappings } from '~/interfaces/ui';
|
|
|
|
|
+
|
|
|
import styles from './CustomNav.module.scss';
|
|
import styles from './CustomNav.module.scss';
|
|
|
|
|
|
|
|
|
|
|
|
|
-function getBreakpointOneLevelLarger(breakpoint) {
|
|
|
|
|
|
|
+function getBreakpointOneLevelLarger(breakpoint: Breakpoint): Omit<Breakpoint, 'xs' | 'sm'> {
|
|
|
switch (breakpoint) {
|
|
switch (breakpoint) {
|
|
|
|
|
+ case 'xs':
|
|
|
|
|
+ return 'sm';
|
|
|
case 'sm':
|
|
case 'sm':
|
|
|
return 'md';
|
|
return 'md';
|
|
|
case 'md':
|
|
case 'md':
|
|
@@ -25,12 +29,18 @@ function getBreakpointOneLevelLarger(breakpoint) {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
-export const CustomNavDropdown = (props) => {
|
|
|
|
|
|
|
+type CustomNavDropdownProps = {
|
|
|
|
|
+ navTabMapping: ICustomNavTabMappings,
|
|
|
|
|
+ activeTab: string,
|
|
|
|
|
+ onNavSelected?: (selectedTabKey: string) => void,
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+export const CustomNavDropdown = (props: CustomNavDropdownProps): JSX.Element => {
|
|
|
const {
|
|
const {
|
|
|
activeTab, navTabMapping, onNavSelected,
|
|
activeTab, navTabMapping, onNavSelected,
|
|
|
} = props;
|
|
} = props;
|
|
|
|
|
|
|
|
- const activeObj = navTabMapping[activeTab];
|
|
|
|
|
|
|
+ const { Icon, i18n } = navTabMapping[activeTab];
|
|
|
|
|
|
|
|
const menuItemClickHandler = useCallback((key) => {
|
|
const menuItemClickHandler = useCallback((key) => {
|
|
|
if (onNavSelected != null) {
|
|
if (onNavSelected != null) {
|
|
@@ -48,16 +58,15 @@ export const CustomNavDropdown = (props) => {
|
|
|
aria-expanded="false"
|
|
aria-expanded="false"
|
|
|
>
|
|
>
|
|
|
<span className="float-left">
|
|
<span className="float-left">
|
|
|
- { activeObj != null && (
|
|
|
|
|
- <><activeObj.Icon /> {activeObj.i18n}</>
|
|
|
|
|
- ) }
|
|
|
|
|
|
|
+ { Icon != null && <Icon /> } {i18n}
|
|
|
</span>
|
|
</span>
|
|
|
</button>
|
|
</button>
|
|
|
<div className="dropdown-menu dropdown-menu-right">
|
|
<div className="dropdown-menu dropdown-menu-right">
|
|
|
{Object.entries(navTabMapping).map(([key, value]) => {
|
|
{Object.entries(navTabMapping).map(([key, value]) => {
|
|
|
|
|
|
|
|
const isActive = activeTab === key;
|
|
const isActive = activeTab === key;
|
|
|
- const isLinkEnabled = value.isLinkEnabled != null ? value.isLinkEnabled(value) : true;
|
|
|
|
|
|
|
+ const _isLinkEnabled = value.isLinkEnabled ?? true;
|
|
|
|
|
+ const isLinkEnabled = typeof _isLinkEnabled === 'boolean' ? _isLinkEnabled : _isLinkEnabled(value);
|
|
|
const { Icon, i18n } = value;
|
|
const { Icon, i18n } = value;
|
|
|
|
|
|
|
|
return (
|
|
return (
|
|
@@ -68,7 +77,7 @@ export const CustomNavDropdown = (props) => {
|
|
|
disabled={!isLinkEnabled}
|
|
disabled={!isLinkEnabled}
|
|
|
onClick={() => menuItemClickHandler(key)}
|
|
onClick={() => menuItemClickHandler(key)}
|
|
|
>
|
|
>
|
|
|
- <Icon /> {i18n}
|
|
|
|
|
|
|
+ { Icon != null && <Icon /> } {i18n}
|
|
|
</button>
|
|
</button>
|
|
|
);
|
|
);
|
|
|
})}
|
|
})}
|
|
@@ -77,23 +86,29 @@ export const CustomNavDropdown = (props) => {
|
|
|
);
|
|
);
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
-CustomNavDropdown.propTypes = {
|
|
|
|
|
- navTabMapping: PropTypes.object.isRequired,
|
|
|
|
|
- activeTab: PropTypes.string,
|
|
|
|
|
- onNavSelected: PropTypes.func,
|
|
|
|
|
-};
|
|
|
|
|
|
|
|
|
|
|
|
+type CustomNavTabProps = {
|
|
|
|
|
+ activeTab: string,
|
|
|
|
|
+ navTabMapping: ICustomNavTabMappings,
|
|
|
|
|
+ onNavSelected?: (selectedTabKey: string) => void,
|
|
|
|
|
+ hideBorderBottom?: boolean,
|
|
|
|
|
+ breakpointToHideInactiveTabsDown?: Breakpoint,
|
|
|
|
|
+ navRightElement?: JSX.Element,
|
|
|
|
|
+};
|
|
|
|
|
|
|
|
-export const CustomNavTab = (props) => {
|
|
|
|
|
- const navContainer = useRef();
|
|
|
|
|
|
|
+export const CustomNavTab = (props: CustomNavTabProps): JSX.Element => {
|
|
|
const [sliderWidth, setSliderWidth] = useState(0);
|
|
const [sliderWidth, setSliderWidth] = useState(0);
|
|
|
const [sliderMarginLeft, setSliderMarginLeft] = useState(0);
|
|
const [sliderMarginLeft, setSliderMarginLeft] = useState(0);
|
|
|
|
|
|
|
|
const {
|
|
const {
|
|
|
- activeTab, navTabMapping, onNavSelected, hideBorderBottom, breakpointToHideInactiveTabsDown, navRightElement,
|
|
|
|
|
|
|
+ activeTab, navTabMapping, onNavSelected,
|
|
|
|
|
+ hideBorderBottom,
|
|
|
|
|
+ breakpointToHideInactiveTabsDown, navRightElement,
|
|
|
} = props;
|
|
} = props;
|
|
|
|
|
|
|
|
- const navTabRefs = useMemo(() => {
|
|
|
|
|
|
|
+ const navContainerRef = useRef<HTMLDivElement>(null);
|
|
|
|
|
+
|
|
|
|
|
+ const navTabRefs: { [key: string]: HTMLAnchorElement } = useMemo(() => {
|
|
|
const obj = {};
|
|
const obj = {};
|
|
|
Object.keys(navTabMapping).forEach((key) => {
|
|
Object.keys(navTabMapping).forEach((key) => {
|
|
|
obj[key] = React.createRef();
|
|
obj[key] = React.createRef();
|
|
@@ -107,9 +122,9 @@ export const CustomNavTab = (props) => {
|
|
|
}
|
|
}
|
|
|
}, [onNavSelected]);
|
|
}, [onNavSelected]);
|
|
|
|
|
|
|
|
- function registerNavLink(key, elm) {
|
|
|
|
|
- if (elm != null) {
|
|
|
|
|
- navTabRefs[key] = elm;
|
|
|
|
|
|
|
+ function registerNavLink(key: string, anchorElem: HTMLAnchorElement | null) {
|
|
|
|
|
+ if (anchorElem != null) {
|
|
|
|
|
+ navTabRefs[key] = anchorElem;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -123,27 +138,28 @@ export const CustomNavTab = (props) => {
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- if (navContainer == null) {
|
|
|
|
|
|
|
+ if (navContainerRef.current == null) {
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- let tempML = 0;
|
|
|
|
|
|
|
+ const navContainer = navContainerRef.current;
|
|
|
|
|
|
|
|
- const styles = Object.entries(navTabRefs).map((el) => {
|
|
|
|
|
- const width = getPercentage(el[1].offsetWidth, navContainer.current.offsetWidth);
|
|
|
|
|
- const marginLeft = tempML;
|
|
|
|
|
- tempML += width;
|
|
|
|
|
- return { width, marginLeft };
|
|
|
|
|
- });
|
|
|
|
|
- const { width, marginLeft } = styles[navTabMapping[activeTab].index];
|
|
|
|
|
|
|
+ let marginLeft = 0;
|
|
|
|
|
+ for (const [key, anchorElem] of Object.entries(navTabRefs)) {
|
|
|
|
|
+ const width = getPercentage(anchorElem.offsetWidth, navContainer.offsetWidth);
|
|
|
|
|
|
|
|
- setSliderWidth(width);
|
|
|
|
|
- setSliderMarginLeft(marginLeft);
|
|
|
|
|
|
|
+ if (key === activeTab) {
|
|
|
|
|
+ setSliderWidth(width);
|
|
|
|
|
+ setSliderMarginLeft(marginLeft);
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
|
|
+ marginLeft += width;
|
|
|
|
|
+ }
|
|
|
}, [activeTab, navTabRefs, navTabMapping]);
|
|
}, [activeTab, navTabRefs, navTabMapping]);
|
|
|
|
|
|
|
|
// determine inactive classes to hide NavItem
|
|
// determine inactive classes to hide NavItem
|
|
|
- const inactiveClassnames = [];
|
|
|
|
|
|
|
+ const inactiveClassnames: string[] = [];
|
|
|
if (breakpointToHideInactiveTabsDown != null) {
|
|
if (breakpointToHideInactiveTabsDown != null) {
|
|
|
const breakpointOneLevelLarger = getBreakpointOneLevelLarger(breakpointToHideInactiveTabsDown);
|
|
const breakpointOneLevelLarger = getBreakpointOneLevelLarger(breakpointToHideInactiveTabsDown);
|
|
|
inactiveClassnames.push('d-none');
|
|
inactiveClassnames.push('d-none');
|
|
@@ -152,12 +168,13 @@ export const CustomNavTab = (props) => {
|
|
|
|
|
|
|
|
return (
|
|
return (
|
|
|
<div className={`grw-custom-nav-tab ${styles['grw-custom-nav-tab']}`}>
|
|
<div className={`grw-custom-nav-tab ${styles['grw-custom-nav-tab']}`}>
|
|
|
- <div ref={navContainer} className="d-flex justify-content-between">
|
|
|
|
|
|
|
+ <div ref={navContainerRef} className="d-flex justify-content-between">
|
|
|
<Nav className="nav-title">
|
|
<Nav className="nav-title">
|
|
|
{Object.entries(navTabMapping).map(([key, value]) => {
|
|
{Object.entries(navTabMapping).map(([key, value]) => {
|
|
|
|
|
|
|
|
const isActive = activeTab === key;
|
|
const isActive = activeTab === key;
|
|
|
- const isLinkEnabled = value.isLinkEnabled != null ? value.isLinkEnabled(value) : true;
|
|
|
|
|
|
|
+ const _isLinkEnabled = value.isLinkEnabled ?? true;
|
|
|
|
|
+ const isLinkEnabled = typeof _isLinkEnabled === 'boolean' ? _isLinkEnabled : _isLinkEnabled(value);
|
|
|
const { Icon, i18n } = value;
|
|
const { Icon, i18n } = value;
|
|
|
|
|
|
|
|
return (
|
|
return (
|
|
@@ -166,7 +183,7 @@ export const CustomNavTab = (props) => {
|
|
|
className={`p-0 ${isActive ? 'active' : inactiveClassnames.join(' ')}`}
|
|
className={`p-0 ${isActive ? 'active' : inactiveClassnames.join(' ')}`}
|
|
|
>
|
|
>
|
|
|
<NavLink type="button" key={key} innerRef={elm => registerNavLink(key, elm)} disabled={!isLinkEnabled} onClick={() => navLinkClickHandler(key)}>
|
|
<NavLink type="button" key={key} innerRef={elm => registerNavLink(key, elm)} disabled={!isLinkEnabled} onClick={() => navLinkClickHandler(key)}>
|
|
|
- <Icon /> {i18n}
|
|
|
|
|
|
|
+ { Icon != null && <Icon /> } {i18n}
|
|
|
</NavLink>
|
|
</NavLink>
|
|
|
</NavItem>
|
|
</NavItem>
|
|
|
);
|
|
);
|
|
@@ -181,27 +198,23 @@ export const CustomNavTab = (props) => {
|
|
|
|
|
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
-CustomNavTab.propTypes = {
|
|
|
|
|
- activeTab: PropTypes.string.isRequired,
|
|
|
|
|
- navTabMapping: PropTypes.object.isRequired,
|
|
|
|
|
- onNavSelected: PropTypes.func,
|
|
|
|
|
- hideBorderBottom: PropTypes.bool,
|
|
|
|
|
- breakpointToHideInactiveTabsDown: PropTypes.oneOf(['xs', 'sm', 'md', 'lg', 'xl']),
|
|
|
|
|
- navRightElement: PropTypes.node,
|
|
|
|
|
-};
|
|
|
|
|
|
|
|
|
|
-CustomNavTab.defaultProps = {
|
|
|
|
|
- hideBorderBottom: false,
|
|
|
|
|
|
|
+type CustomNavProps = {
|
|
|
|
|
+ activeTab: string,
|
|
|
|
|
+ navTabMapping: ICustomNavTabMappings,
|
|
|
|
|
+ onNavSelected?: (selectedTabKey: string) => void,
|
|
|
|
|
+ hideBorderBottom?: boolean,
|
|
|
|
|
+ breakpointToHideInactiveTabsDown?: Breakpoint,
|
|
|
|
|
+ breakpointToSwitchDropdownDown?: Breakpoint,
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
-
|
|
|
|
|
-const CustomNav = (props) => {
|
|
|
|
|
|
|
+const CustomNav = (props: CustomNavProps): JSX.Element => {
|
|
|
|
|
|
|
|
const tabClassnames = ['d-none'];
|
|
const tabClassnames = ['d-none'];
|
|
|
const dropdownClassnames = ['d-block'];
|
|
const dropdownClassnames = ['d-block'];
|
|
|
|
|
|
|
|
// determine classes to show/hide
|
|
// determine classes to show/hide
|
|
|
- const breakpointOneLevelLarger = getBreakpointOneLevelLarger(props.breakpointToSwitchDropdownDown);
|
|
|
|
|
|
|
+ const breakpointOneLevelLarger = getBreakpointOneLevelLarger(props.breakpointToSwitchDropdownDown ?? 'sm');
|
|
|
tabClassnames.push(`d-${breakpointOneLevelLarger}-block`);
|
|
tabClassnames.push(`d-${breakpointOneLevelLarger}-block`);
|
|
|
dropdownClassnames.push(`d-${breakpointOneLevelLarger}-none`);
|
|
dropdownClassnames.push(`d-${breakpointOneLevelLarger}-none`);
|
|
|
|
|
|
|
@@ -218,19 +231,4 @@ const CustomNav = (props) => {
|
|
|
|
|
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
-CustomNav.propTypes = {
|
|
|
|
|
- activeTab: PropTypes.string.isRequired,
|
|
|
|
|
- navTabMapping: PropTypes.object.isRequired,
|
|
|
|
|
- onNavSelected: PropTypes.func,
|
|
|
|
|
- hideBorderBottom: PropTypes.bool,
|
|
|
|
|
- breakpointToHideInactiveTabsDown: PropTypes.oneOf(['xs', 'sm', 'md', 'lg', 'xl']),
|
|
|
|
|
- breakpointToSwitchDropdownDown: PropTypes.oneOf(['xs', 'sm', 'md', 'lg', 'xl']),
|
|
|
|
|
-};
|
|
|
|
|
-
|
|
|
|
|
-CustomNav.defaultProps = {
|
|
|
|
|
- hideBorderBottom: false,
|
|
|
|
|
- breakpointToSwitchDropdownDown: 'sm',
|
|
|
|
|
-};
|
|
|
|
|
-
|
|
|
|
|
-
|
|
|
|
|
export default CustomNav;
|
|
export default CustomNav;
|