Item.tsx 12 KB

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