123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442 |
- /* eslint-disable max-lines-per-function */
- import BaseComponent, { BaseProps } from '../_base/baseComponent';
- import React, { Children, ReactElement, ReactNode } from 'react';
- import PropTypes from 'prop-types';
- import cls from 'classnames';
- import { noop, get, isEqual } from 'lodash';
- import NavigationFoundation, { NavigationAdapter } from '@douyinfe/semi-foundation/navigation/foundation';
- import { strings, cssClasses, numbers } from '@douyinfe/semi-foundation/navigation/constants';
- import SubNav, { SubNavProps } from './SubNav';
- import Item, { NavItemProps } from './Item';
- import Footer, { NavFooterProps } from './Footer';
- import Header, { NavHeaderProps } from './Header';
- import NavContext from './nav-context';
- import LocaleConsumer from '../locale/localeConsumer';
- import '@douyinfe/semi-foundation/navigation/navigation.scss';
- export type { CollapseButtonProps } from './CollapseButton';
- export type { NavFooterProps } from './Footer';
- export type { NavHeaderProps } from './Header';
- export type { NavItemProps } from './Item';
- export type { SubNavProps } from './SubNav';
- export type Mode = 'vertical' | 'horizontal';
- export interface OnSelectedData {
- itemKey: React.ReactText;
- selectedKeys: React.ReactText[];
- selectedItems: (NavItemProps | SubNavProps)[];
- domEvent: React.MouseEvent;
- isOpen: boolean
- }
- export interface SubNavPropsWithItems extends SubNavProps {
- items?: (SubNavPropsWithItems | string)[]
- }
- export interface NavItemPropsWithItems extends NavItemProps {
- items?: (NavItemPropsWithItems | string)[]
- }
- export type NavItems = (string | SubNavPropsWithItems | NavItemPropsWithItems)[];
- export interface NavProps extends BaseProps {
- bodyStyle?: React.CSSProperties;
- children?: React.ReactNode;
-
- defaultIsCollapsed?: boolean;
- defaultOpenKeys?: React.ReactText[];
- defaultSelectedKeys?: React.ReactText[];
- expandIcon?: React.ReactNode;
- footer?: React.ReactNode | NavFooterProps;
- header?: React.ReactNode | NavHeaderProps;
- isCollapsed?: boolean;
- items?: NavItems;
- limitIndent?: boolean;
- mode?: Mode;
- multiple?: boolean;
- openKeys?: React.ReactText[];
- prefixCls?: string;
- selectedKeys?: React.ReactText[];
- subNavCloseDelay?: number;
- subNavMotion?: boolean;
- subNavOpenDelay?: number;
- toggleIconPosition?: string;
- tooltipHideDelay?: number;
- tooltipShowDelay?: number;
- getPopupContainer?: () => HTMLElement;
- onClick?: (data: { itemKey?: React.ReactText; domEvent?: MouseEvent; isOpen?: boolean }) => void;
- onCollapseChange?: (isCollapse: boolean) => void;
- onDeselect?: (data?: any) => void;
- onOpenChange?: (data: { itemKey?: (string | number); openKeys?: (string | number)[]; domEvent?: MouseEvent; isOpen?: boolean }) => void;
- onSelect?: (data: OnSelectedData) => void;
- renderWrapper?: ({ itemElement, isSubNav, isInSubNav, props }: { itemElement: ReactElement;isInSubNav: boolean; isSubNav: boolean; props: NavItemProps | SubNavProps }) => ReactNode
- }
- export interface NavState {
- isCollapsed: boolean;
- // calc state
- openKeys: (string | number)[];
- items: any[];
- itemKeysMap: { [itemKey: string]: (string | number)[] };
- selectedKeys: (string | number)[]
- }
- function createAddKeysFn(context: Nav, keyName: string | number) {
- return function addKeys(...keys: (string | number)[]) {
- const handleKeys = new Set(context.state[keyName]);
- keys.forEach(key => key && handleKeys.add(key));
- context.setState({ [keyName]: Array.from(handleKeys) } as any);
- };
- }
- function createRemoveKeysFn(context: Nav, keyName: string) {
- return function removeKeys(...keys: string[]) {
- const handleKeys = new Set(context.state[keyName]);
- keys.forEach(key => key && handleKeys.delete(key));
- context.setState({ [keyName]: Array.from(handleKeys) } as any);
- };
- }
- const { hasOwnProperty } = Object.prototype;
- class Nav extends BaseComponent<NavProps, NavState> {
- static Sub = SubNav;
- static Item = Item;
- static Header = Header;
- static Footer = Footer;
- static propTypes = {
- collapseIcon: PropTypes.node,
- // Initial expanded SubNav navigation key array
- defaultOpenKeys: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])),
- openKeys: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])),
- // Initial selected navigation key array
- defaultSelectedKeys: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])),
- expandIcon: PropTypes.node,
- selectedKeys: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])),
- // Navigation type, now supports vertical, horizontal
- mode: PropTypes.oneOf([...strings.MODE]),
- // Triggered when selecting a navigation item
- onSelect: PropTypes.func,
- // Triggered when clicking a navigation item
- onClick: PropTypes.func,
- // SubNav expand/close callback
- onOpenChange: PropTypes.func,
- // Array of options (nested options can continue)
- items: PropTypes.array,
- // Is it in the state of being stowed to the sidebar
- isCollapsed: PropTypes.bool,
- defaultIsCollapsed: PropTypes.bool,
- onCollapseChange: PropTypes.func,
- multiple: PropTypes.bool,
- onDeselect: PropTypes.func,
- subNavMotion: PropTypes.oneOfType([PropTypes.bool, PropTypes.object, PropTypes.func]),
- subNavCloseDelay: PropTypes.number,
- subNavOpenDelay: PropTypes.number,
- tooltipShowDelay: PropTypes.number,
- tooltipHideDelay: PropTypes.number,
- children: PropTypes.node,
- style: PropTypes.object,
- bodyStyle: PropTypes.object,
- className: PropTypes.string,
- toggleIconPosition: PropTypes.string,
- prefixCls: PropTypes.string,
- header: PropTypes.oneOfType([PropTypes.node, PropTypes.object]),
- footer: PropTypes.oneOfType([PropTypes.node, PropTypes.object]),
- limitIndent: PropTypes.bool,
- getPopupContainer: PropTypes.func,
- };
- static defaultProps = {
- subNavCloseDelay: numbers.DEFAULT_SUBNAV_CLOSE_DELAY,
- subNavOpenDelay: numbers.DEFAULT_SUBNAV_OPEN_DELAY,
- tooltipHideDelay: numbers.DEFAULT_TOOLTIP_HIDE_DELAY,
- tooltipShowDelay: numbers.DEFAULT_TOOLTIP_SHOW_DELAY,
- onCollapseChange: noop,
- onSelect: noop,
- onClick: noop,
- onOpenChange: noop,
- toggleIconPosition: 'right',
- limitIndent: true,
- prefixCls: cssClasses.PREFIX,
- subNavMotion: true,
- // isOpen: false,
- mode: strings.MODE_VERTICAL,
- // defaultOpenKeys: [],
- // defaultSelectedKeys: [],
- // items: [],
- };
- itemsChanged: boolean;
- constructor(props: NavProps) {
- super(props);
- this.foundation = new NavigationFoundation(this.adapter);
- this.itemsChanged = true;
- const { isCollapsed, defaultIsCollapsed, items, children } = props;
- const initState = {
- isCollapsed: Boolean(this.isControlled('isCollapsed') ? isCollapsed : defaultIsCollapsed),
- // calc state
- openKeys: [],
- items: [],
- itemKeysMap: {}, // itemKey to parentKeys
- selectedKeys: [],
- };
- this.state = { ...initState };
- if (items && items.length || children) {
- const calcState = this.foundation.init('constructor');
- this.state = {
- ...initState,
- ...calcState,
- };
- }
- }
- static getDerivedStateFromProps(props: NavProps, state: NavState) {
- const willUpdateState: Partial<NavState> = {};
- if (hasOwnProperty.call(props, 'isCollapsed') && props.isCollapsed !== state.isCollapsed) {
- willUpdateState.isCollapsed = props.isCollapsed;
- }
- return willUpdateState;
- }
- componentDidMount() {
- // override BaseComponent
- }
- componentDidUpdate(prevProps: NavProps, prevState: NavState) {
- if (prevProps.items !== this.props.items || prevProps.children !== this.props.children) {
- this.foundation.init();
- } else {
- this.foundation.handleItemsChange(false);
- const { selectedKeys } = this.state;
- if (this.props.selectedKeys && !isEqual(prevProps.selectedKeys, this.props.selectedKeys)) {
- this.adapter.updateSelectedKeys(this.props.selectedKeys);
- }
- if (this.props.openKeys && !isEqual(prevProps.openKeys, this.props.openKeys)) {
- this.adapter.updateOpenKeys(this.props.openKeys);
- }
- if (!isEqual(selectedKeys, prevState.selectedKeys)) {
- const parentSelectKeys = this.foundation.selectLevelZeroParentKeys(null, ...selectedKeys);
- this.adapter.addSelectedKeys(...parentSelectKeys);
- }
- }
- }
- get adapter(): NavigationAdapter<NavProps, NavState> {
- return {
- ...super.adapter,
- notifySelect: (...args) => this.props.onSelect(...args),
- notifyOpenChange: (...args) => this.props.onOpenChange(...args),
- setIsCollapsed: isCollapsed => this.setState({ isCollapsed }),
- notifyCollapseChange: (...args) => this.props.onCollapseChange(...args),
- updateItems: items => this.setState({ items: [...items] }),
- setItemKeysMap: itemKeysMap => this.setState({ itemKeysMap: { ...itemKeysMap } }),
- addSelectedKeys: createAddKeysFn(this, 'selectedKeys'),
- removeSelectedKeys: createRemoveKeysFn(this, 'selectedKeys'),
- updateSelectedKeys: selectedKeys => this.setState({ selectedKeys: [...selectedKeys] }),
- updateOpenKeys: openKeys => this.setState({ openKeys: [...openKeys] }),
- addOpenKeys: createAddKeysFn(this, 'openKeys'),
- removeOpenKeys: createRemoveKeysFn(this, 'openKeys'),
- setItemsChanged: isChanged => {
- this.itemsChanged = isChanged;
- },
- };
- }
- /**
- * Render navigation items recursively
- *
- * @param {NavItem[]} items
- * @returns {JSX.Element}
- */
- renderItems(items: (SubNavPropsWithItems | NavItemPropsWithItems)[] = [], level = 0) {
- const { expandIcon } = this.props;
- const finalDom = (
- <>
- {items.map((item, idx) => {
- if (Array.isArray(item.items) && item.items.length) {
- return (
- <SubNav
- key={item.itemKey || String(level) + idx}
- {...item as SubNavPropsWithItems}
- level={level}
- expandIcon={expandIcon}
- >
- {this.renderItems(item.items as (SubNavPropsWithItems | NavItemPropsWithItems)[], level + 1)}
- </SubNav>
- );
- } else {
- return <Item key={item.itemKey || String(level) + idx} {...item as NavItemPropsWithItems} level={level} />;
- }
- })}
- </>
- );
- return finalDom;
- }
- onCollapseChange = () => {
- this.foundation.handleCollapseChange();
- };
- render() {
- const {
- children: originChildren,
- mode,
- onOpenChange,
- onSelect,
- onClick,
- style,
- className,
- subNavCloseDelay,
- subNavOpenDelay,
- subNavMotion,
- tooltipShowDelay,
- tooltipHideDelay,
- prefixCls,
- bodyStyle,
- footer,
- header,
- toggleIconPosition,
- limitIndent,
- renderWrapper,
- getPopupContainer,
- ...rest
- } = this.props;
- const { selectedKeys, openKeys, items, isCollapsed } = this.state;
- const {
- updateOpenKeys,
- addOpenKeys,
- removeOpenKeys,
- updateSelectedKeys,
- addSelectedKeys,
- removeSelectedKeys,
- } = this.adapter;
- const finalStyle = { ...style };
- let children: React.ReactNode[] = Children.toArray(originChildren);
- const footers: React.ReactNode[] = [];
- const headers: React.ReactNode[] = [];
- if (React.isValidElement(footer)) {
- footers.push(<Footer key={0}>{footer}</Footer>);
- } else if (footer && typeof footer === 'object') {
- footers.push(<Footer key={0} {...footer} />);
- }
- if (React.isValidElement(header)) {
- headers.push(<Header key={0}>{header}</Header>);
- } else if (header && typeof header === 'object') {
- headers.push(<Header key={0} {...header} />);
- }
- if (Array.isArray(children) && children.length) {
- children = [...children];
- let childrenLength = children.length;
- for (let i = 0; i < childrenLength; i++) {
- const child = children[i];
- if ((child as any).type === Footer || get(child, 'type.elementType') === 'NavFooter') {
- footers.push(child);
- children.splice(i, 1);
- i--;
- childrenLength--;
- } else if ((child as any).type === Header || get(child, 'type.elementType') === 'NavHeader') {
- headers.push(child);
- children.splice(i, 1);
- i--;
- childrenLength--;
- }
- }
- }
- const finalCls = cls(prefixCls, className, {
- [`${prefixCls}-collapsed`]: isCollapsed,
- [`${prefixCls}-horizontal`]: mode === 'horizontal',
- [`${prefixCls}-vertical`]: mode === 'vertical',
- });
- const headerListOuterCls = cls(`${prefixCls}-header-list-outer`, {
- [`${prefixCls}-header-list-outer-collapsed`]: isCollapsed,
- });
- if (this.itemsChanged) {
- this.adapter.setCache('itemElems', this.renderItems(items));
- }
- return (
- <LocaleConsumer componentName="Navigation">
- {locale => (
- <NavContext.Provider
- value={{
- subNavCloseDelay,
- subNavOpenDelay,
- subNavMotion,
- tooltipShowDelay,
- tooltipHideDelay,
- openKeys,
- openKeysIsControlled: this.isControlled('openKeys') && mode === 'vertical' && !isCollapsed,
- // canUpdateOpenKeys: mode === 'vertical' && !isCollapsed,
- canUpdateOpenKeys: true,
- selectedKeys,
- selectedKeysIsControlled: this.isControlled('selectedKeys'),
- isCollapsed,
- onCollapseChange: this.onCollapseChange,
- mode,
- onSelect,
- onOpenChange,
- updateOpenKeys,
- addOpenKeys,
- removeOpenKeys,
- updateSelectedKeys,
- addSelectedKeys,
- removeSelectedKeys,
- onClick,
- locale,
- prefixCls,
- toggleIconPosition,
- limitIndent,
- renderWrapper,
- getPopupContainer
- } as any}
- >
- <div className={finalCls} style={finalStyle} {...this.getDataAttr(rest)}>
- <div className={`${prefixCls}-inner`}>
- <div className={headerListOuterCls}>
- {headers}
- <div style={bodyStyle} className={`${prefixCls}-list-wrapper`}>
- <ul role="menu" aria-orientation={mode} className={`${prefixCls}-list`}>
- {this.adapter.getCache('itemElems')}
- {children}
- </ul>
- </div>
- </div>
- {footers}
- </div>
- </div>
- </NavContext.Provider>
- )}
- </LocaleConsumer>
- );
- }
- }
- export default Nav;
|