Item.tsx 11 KB

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