index.tsx 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. import React from 'react';
  2. import classnames from 'classnames';
  3. import PropTypes from 'prop-types';
  4. import { cssClasses, strings, numbers } from '@douyinfe/semi-foundation/dropdown/constants';
  5. import BaseComponent from '../_base/baseComponent';
  6. import Tooltip, { Position, TooltipProps, Trigger } from '../tooltip/index';
  7. import { numbers as tooltipNumbers } from '@douyinfe/semi-foundation/tooltip/constants';
  8. import Foundation from '@douyinfe/semi-foundation/dropdown/foundation';
  9. import DropdownMenu from './dropdownMenu';
  10. import DropdownItem, { DropdownItemProps } from './dropdownItem';
  11. import DropdownDivider, { DropdownDividerProps } from './dropdownDivider';
  12. import DropdownTitle, { DropdownTitleProps } from './dropdownTitle';
  13. import DropdownContext from './context';
  14. import '@douyinfe/semi-foundation/dropdown/dropdown.scss';
  15. import { noop, get } from 'lodash-es';
  16. import { Motion } from '../_base/base';
  17. const positionSet = strings.POSITION_SET;
  18. const triggerSet = strings.TRIGGER_SET;
  19. export { DropdownDividerProps } from './dropdownDivider';
  20. export { DropdownItemProps, Type } from './dropdownItem';
  21. export { DropdownMenuProps } from './dropdownMenu';
  22. export { DropdownTitleProps } from './dropdownTitle';
  23. export interface DropDownMenuItemBasic {
  24. node: 'title' | 'item' | 'divider';
  25. name?: string;
  26. }
  27. export type DropDownMenuItem = DropDownMenuItemBasic & DropdownItemProps & DropdownDividerProps & DropdownTitleProps;
  28. export interface DropdownProps extends TooltipProps {
  29. render?: React.ReactNode;
  30. children?: React.ReactNode;
  31. visible?: boolean;
  32. position?: Position;
  33. getPopupContainer?: () => HTMLElement;
  34. mouseEnterDelay?: number;
  35. mouseLeaveDelay?: number;
  36. menu?: DropDownMenuItem[];
  37. trigger?: Trigger;
  38. zIndex?: number;
  39. motion?: Motion;
  40. className?: string;
  41. contentClassName?: string | any[];
  42. style?: React.CSSProperties;
  43. onVisibleChange?: (visible: boolean) => void;
  44. rePosKey?: string | number;
  45. showTick?: boolean;
  46. }
  47. interface DropdownState {
  48. popVisible: boolean;
  49. }
  50. class Dropdown extends BaseComponent<DropdownProps, DropdownState> {
  51. static Menu = DropdownMenu;
  52. static Item = DropdownItem;
  53. static Divider = DropdownDivider;
  54. static Title = DropdownTitle;
  55. static contextType = DropdownContext;
  56. static propTypes = {
  57. render: PropTypes.node,
  58. children: PropTypes.node,
  59. visible: PropTypes.bool,
  60. position: PropTypes.oneOf(positionSet),
  61. getPopupContainer: PropTypes.func,
  62. mouseEnterDelay: PropTypes.number,
  63. mouseLeaveDelay: PropTypes.number,
  64. trigger: PropTypes.oneOf(triggerSet),
  65. zIndex: PropTypes.number,
  66. motion: PropTypes.oneOfType([PropTypes.bool, PropTypes.func, PropTypes.object]),
  67. className: PropTypes.string,
  68. contentClassName: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
  69. style: PropTypes.object,
  70. onVisibleChange: PropTypes.func,
  71. rePosKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
  72. showTick: PropTypes.bool,
  73. prefixCls: PropTypes.string,
  74. spacing: PropTypes.number,
  75. menu: PropTypes.array,
  76. };
  77. static defaultProps = {
  78. onVisibleChange: noop,
  79. prefixCls: cssClasses.PREFIX,
  80. zIndex: tooltipNumbers.DEFAULT_Z_INDEX,
  81. motion: true,
  82. trigger: 'hover',
  83. position: 'bottom',
  84. mouseLeaveDelay: strings.DEFAULT_LEAVE_DELAY,
  85. showTick: false,
  86. };
  87. constructor(props: DropdownProps) {
  88. super(props);
  89. this.state = {
  90. popVisible: props.visible,
  91. };
  92. this.foundation = new Foundation(this.adapter);
  93. }
  94. get adapter() {
  95. return {
  96. ...super.adapter,
  97. setPopVisible: (popVisible: boolean) => this.setState({ popVisible }),
  98. notifyVisibleChange: (visible: boolean) => this.props.onVisibleChange(visible),
  99. };
  100. }
  101. handleVisibleChange = (visible: boolean) => this.foundation.handleVisibleChange(visible);
  102. renderContent() {
  103. const { render, menu, contentClassName, style, showTick, prefixCls } = this.props;
  104. const className = classnames(prefixCls, contentClassName);
  105. const { level = 0 } = this.context;
  106. const contextValue = { showTick, level: level + 1 };
  107. let content = null;
  108. if (React.isValidElement(render)) {
  109. content = render;
  110. } else if (Array.isArray(menu)) {
  111. content = this.renderMenu();
  112. }
  113. return (
  114. <DropdownContext.Provider value={contextValue}>
  115. <div className={className} style={style}>
  116. <div className={`${prefixCls}-content`}>{content}</div>
  117. </div>
  118. </DropdownContext.Provider>
  119. );
  120. }
  121. renderMenu() {
  122. const { menu } = this.props;
  123. const content = menu.map((m, index) => {
  124. switch (m.node) {
  125. case 'title': {
  126. const { name, node, ...rest } = m;
  127. return (
  128. <Dropdown.Title {...rest} key={node + name + index}>
  129. {name}
  130. </Dropdown.Title>
  131. );
  132. }
  133. case 'item': {
  134. const { node, name, ...rest } = m;
  135. return (
  136. <Dropdown.Item {...rest} key={node + name + index}>
  137. {name}
  138. </Dropdown.Item>
  139. );
  140. }
  141. case 'divider': {
  142. return <Dropdown.Divider key={m.node + index} />;
  143. }
  144. default:
  145. return null;
  146. }
  147. });
  148. return <Dropdown.Menu>{content}</Dropdown.Menu>;
  149. }
  150. renderPopCard() {
  151. const { render, contentClassName, style, showTick, prefixCls } = this.props;
  152. const className = classnames(prefixCls, contentClassName);
  153. const { level = 0 } = this.context;
  154. const contextValue = { showTick, level: level + 1 };
  155. return (
  156. <DropdownContext.Provider value={contextValue}>
  157. <div className={className} style={style}>
  158. <div className={`${prefixCls}-content`}>{render}</div>
  159. </div>
  160. </DropdownContext.Provider>
  161. );
  162. }
  163. render() {
  164. const {
  165. children,
  166. position,
  167. trigger,
  168. onVisibleChange,
  169. zIndex,
  170. className,
  171. motion,
  172. style,
  173. prefixCls,
  174. ...attr
  175. } = this.props;
  176. let { spacing } = this.props;
  177. const { level } = this.context;
  178. const { popVisible } = this.state;
  179. const pop = this.renderContent();
  180. if (level > 0) {
  181. spacing = typeof spacing === 'number' ? spacing : numbers.NESTED_SPACING;
  182. } else if (spacing === null || typeof spacing === 'undefined') {
  183. spacing = numbers.SPACING;
  184. }
  185. return (
  186. <Tooltip
  187. zIndex={zIndex}
  188. motion={motion}
  189. content={pop}
  190. className={className}
  191. prefixCls={prefixCls}
  192. spacing={spacing}
  193. position={position}
  194. trigger={trigger}
  195. onVisibleChange={this.handleVisibleChange}
  196. showArrow={false}
  197. {...attr}
  198. >
  199. {React.isValidElement(children) ?
  200. React.cloneElement(children, {
  201. className: classnames(get(children, 'props.className'), {
  202. [`${prefixCls}-showing`]: popVisible,
  203. }),
  204. }) :
  205. children}
  206. </Tooltip>
  207. );
  208. }
  209. }
  210. export default Dropdown;