index.tsx 8.7 KB

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