Item.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  1. /* eslint-disable max-len */
  2. /* eslint-disable no-nested-ternary */
  3. import BaseComponent, { BaseProps } from '../_base/baseComponent';
  4. import React from 'react';
  5. import PropTypes from 'prop-types';
  6. import cls from 'classnames';
  7. import { times, noop } from 'lodash';
  8. import isNullOrUndefined from '@douyinfe/semi-foundation/utils/isNullOrUndefined';
  9. import { cloneDeep, isSemiIcon } from '../_utils';
  10. import ItemFoundation, { ItemProps, SelectedItemProps, ItemAdapter } from '@douyinfe/semi-foundation/navigation/itemFoundation';
  11. import { strings, cssClasses } from '@douyinfe/semi-foundation/navigation/constants';
  12. import Tooltip from '../tooltip';
  13. import NavContext from './nav-context';
  14. import Dropdown from '../dropdown';
  15. const clsPrefix = `${cssClasses.PREFIX}-item`;
  16. export interface NavItemProps extends ItemProps, BaseProps {
  17. children?: React.ReactNode;
  18. disabled?: boolean;
  19. forwardRef?: (ele: HTMLLIElement) => void;
  20. icon?: React.ReactNode;
  21. itemKey?: React.ReactText;
  22. level?: number;
  23. link?: string;
  24. linkOptions?: React.HTMLAttributes<HTMLLinkElement>;
  25. text?: React.ReactNode;
  26. tooltipHideDelay?: number;
  27. tooltipShowDelay?: number;
  28. onClick?(clickItems: SelectedData): void;
  29. onMouseEnter?: React.MouseEventHandler<HTMLLIElement>;
  30. onMouseLeave?: React.MouseEventHandler<HTMLLIElement>;
  31. }
  32. export interface SelectedData extends SelectedItemProps<NavItemProps> {
  33. text?: React.ReactNode;
  34. }
  35. export interface NavItemState {
  36. tooltipShow: boolean;
  37. }
  38. export default class NavItem extends BaseComponent<NavItemProps, NavItemState> {
  39. static contextType = NavContext;
  40. static propTypes = {
  41. text: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
  42. itemKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
  43. onClick: PropTypes.func,
  44. onMouseEnter: PropTypes.func,
  45. onMouseLeave: PropTypes.func,
  46. children: PropTypes.node,
  47. icon: PropTypes.oneOfType([PropTypes.node]),
  48. className: PropTypes.string,
  49. toggleIcon: PropTypes.string,
  50. style: PropTypes.object,
  51. forwardRef: PropTypes.func,
  52. indent: PropTypes.oneOfType([PropTypes.bool, PropTypes.number]),
  53. isCollapsed: PropTypes.bool, // Is it in a state of folding to the side
  54. isSubNav: PropTypes.bool, // Whether to navigate for children
  55. link: PropTypes.string,
  56. linkOptions: PropTypes.object,
  57. disabled: PropTypes.bool,
  58. };
  59. static defaultProps = {
  60. isSubNav: false,
  61. indent: false,
  62. forwardRef: noop,
  63. isCollapsed: false,
  64. onClick: noop,
  65. onMouseEnter: noop,
  66. onMouseLeave: noop,
  67. disabled: false,
  68. };
  69. foundation: ItemFoundation;
  70. constructor(props: NavItemProps) {
  71. super(props);
  72. this.state = {
  73. tooltipShow: false,
  74. };
  75. this.foundation = new ItemFoundation(this.adapter);
  76. }
  77. _invokeContextFunc(funcName: string, ...args: any[]) {
  78. if (funcName && this.context && typeof this.context[funcName] === 'function') {
  79. return this.context[funcName](...args);
  80. }
  81. return null;
  82. }
  83. get adapter(): ItemAdapter<NavItemProps, NavItemState> {
  84. return {
  85. ...super.adapter,
  86. cloneDeep,
  87. updateTooltipShow: tooltipShow => this.setState({ tooltipShow }),
  88. updateSelected: _selected => this._invokeContextFunc('updateSelectedKeys', [this.props.itemKey]),
  89. updateGlobalSelectedKeys: keys => this._invokeContextFunc('updateSelectedKeys', [...keys]),
  90. getSelectedKeys: () => this.context && this.context.selectedKeys,
  91. getSelectedKeysIsControlled: () => this.context && this.context.selectedKeysIsControlled,
  92. notifyGlobalOnSelect: (...args) => this._invokeContextFunc('onSelect', ...args),
  93. notifyGlobalOnClick: (...args) => this._invokeContextFunc('onClick', ...args),
  94. notifyClick: (...args) => this.props.onClick(...args),
  95. notifyMouseEnter: (...args) => this.props.onMouseEnter(...args),
  96. notifyMouseLeave: (...args) => this.props.onMouseLeave(...args),
  97. getIsCollapsed: () => this.props.isCollapsed || Boolean(this.context && this.context.isCollapsed) || false,
  98. getSelected: () =>
  99. Boolean(this.context && this.context.selectedKeys && this.context.selectedKeys.includes(this.props.itemKey)),
  100. getIsOpen: () =>
  101. Boolean(this.context && this.context.openKeys && this.context.openKeys.includes(this.props.itemKey)),
  102. };
  103. }
  104. renderIcon(icon: React.ReactNode, pos: string, isToggleIcon = false) {
  105. if (this.props.isSubNav) {
  106. return null;
  107. }
  108. if (!icon && this.context.mode === strings.MODE_HORIZONTAL) {
  109. return null;
  110. }
  111. let iconSize = 'large';
  112. if (pos === strings.ICON_POS_RIGHT) {
  113. iconSize = 'default';
  114. }
  115. const className = cls(`${clsPrefix}-icon`, {
  116. [`${clsPrefix}-icon-toggle-${this.context.toggleIconPosition}`]: isToggleIcon,
  117. [`${clsPrefix}-icon-info`]: !isToggleIcon
  118. });
  119. return (
  120. <i className={className}>
  121. {isSemiIcon(icon) ? React.cloneElement((icon as React.ReactElement), {size: (icon as React.ReactElement).props.size || iconSize}) : icon}
  122. </i>
  123. );
  124. }
  125. setItemRef = (ref: HTMLLIElement) => {
  126. // console.log('Item - setItemRef()', ref);
  127. this.props.forwardRef && this.props.forwardRef(ref);
  128. };
  129. wrapTooltip = (node: React.ReactNode) => {
  130. const { text, tooltipHideDelay, tooltipShowDelay } = this.props;
  131. return (
  132. <Tooltip
  133. content={text}
  134. position="right"
  135. trigger={'hover'}
  136. mouseEnterDelay={tooltipShowDelay}
  137. mouseLeaveDelay={tooltipHideDelay}
  138. >
  139. {node}
  140. </Tooltip>
  141. );
  142. };
  143. handleClick = (e: React.MouseEvent) => this.foundation.handleClick(e);
  144. handleKeyPress = (e: React.KeyboardEvent) => this.foundation.handleKeyPress(e);
  145. render() {
  146. const {
  147. text,
  148. children,
  149. icon,
  150. toggleIcon,
  151. className,
  152. isSubNav,
  153. style,
  154. indent,
  155. onMouseEnter,
  156. onMouseLeave,
  157. link,
  158. linkOptions,
  159. disabled,
  160. level = 0,
  161. } = this.props;
  162. const { mode, isInSubNav, prefixCls, limitIndent } = this.context;
  163. const isCollapsed = this.adapter.getIsCollapsed();
  164. const selected = this.adapter.getSelected();
  165. let itemChildren = null;
  166. if (!isNullOrUndefined(children)) {
  167. itemChildren = children;
  168. } else {
  169. let placeholderIcons = null;
  170. if (mode === strings.MODE_VERTICAL && !limitIndent) {
  171. const iconAmount = (icon && !indent) ? level : level - 1;
  172. placeholderIcons = times(iconAmount, () => this.renderIcon(null, strings.ICON_POS_RIGHT, false));
  173. }
  174. itemChildren = (
  175. <>
  176. {placeholderIcons}
  177. {this.context.toggleIconPosition === strings.TOGGLE_ICON_LEFT && this.renderIcon(toggleIcon, strings.ICON_POS_RIGHT, true)}
  178. {icon || indent || isInSubNav ? this.renderIcon(icon, strings.ICON_POS_LEFT) : null}
  179. {!isNullOrUndefined(text) ? <span className={`${cssClasses.PREFIX}-item-text`}>{text}</span> : ''}
  180. {this.context.toggleIconPosition === strings.TOGGLE_ICON_RIGHT && this.renderIcon(toggleIcon, strings.ICON_POS_RIGHT, true)}
  181. </>
  182. );
  183. }
  184. if (typeof link === 'string') {
  185. itemChildren = (
  186. <a className={`${prefixCls}-item-link`} href={link} {...(linkOptions as any)}>
  187. {itemChildren}
  188. </a>
  189. );
  190. }
  191. let itemDom: React.ReactNode = '';
  192. if (isInSubNav && (isCollapsed || mode === strings.MODE_HORIZONTAL)) {
  193. const popoverItemCls = cls({
  194. [clsPrefix]: true,
  195. [`${clsPrefix}-sub`]: isSubNav,
  196. [`${clsPrefix}-selected`]: selected,
  197. [`${clsPrefix}-collapsed`]: isCollapsed,
  198. [`${clsPrefix}-disabled`]: disabled,
  199. });
  200. itemDom = (
  201. <Dropdown.Item
  202. selected={selected}
  203. active={selected}
  204. forwardRef={this.setItemRef}
  205. className={popoverItemCls}
  206. onClick={this.handleClick}
  207. onMouseEnter={onMouseEnter}
  208. onMouseLeave={onMouseLeave}
  209. disabled={disabled}
  210. >
  211. {itemChildren}
  212. </Dropdown.Item>
  213. );
  214. } else {
  215. // Items are divided into normal and sub-wrap
  216. const popoverItemCls = cls(`${className || `${clsPrefix }-normal`}`, {
  217. [clsPrefix]: true,
  218. [`${clsPrefix}-sub`]: isSubNav,
  219. [`${clsPrefix}-selected`]: selected && !isSubNav,
  220. [`${clsPrefix}-collapsed`]: isCollapsed,
  221. [`${clsPrefix}-disabled`]: disabled,
  222. });
  223. const ariaProps = {
  224. 'aria-disabled': disabled,
  225. };
  226. if (isSubNav) {
  227. const isOpen = this.adapter.getIsOpen();
  228. ariaProps['aria-expanded'] = isOpen;
  229. }
  230. itemDom = (
  231. <li
  232. role="menuitem"
  233. tabIndex={-1}
  234. {...ariaProps}
  235. style={style}
  236. ref={this.setItemRef}
  237. className={popoverItemCls}
  238. onClick={this.handleClick}
  239. onMouseEnter={onMouseEnter}
  240. onMouseLeave={onMouseLeave}
  241. onKeyPress={this.handleKeyPress}
  242. >
  243. {itemChildren}
  244. </li>
  245. );
  246. }
  247. // Display Tooltip when disabled and SubNav
  248. if (isCollapsed && !isInSubNav && !isSubNav || isCollapsed && isSubNav && disabled) {
  249. itemDom = this.wrapTooltip(itemDom);
  250. }
  251. return itemDom;
  252. }
  253. }