index.tsx 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. import React, { PureComponent } from 'react';
  2. import { createPortal } from 'react-dom';
  3. import { BASE_CLASS_PREFIX } from '@douyinfe/semi-foundation/base/constants';
  4. import PropTypes from 'prop-types';
  5. import classnames from 'classnames';
  6. import ConfigContext, { ContextValue } from '../configProvider/context';
  7. import '@douyinfe/semi-foundation/_portal/portal.scss';
  8. export interface PortalProps {
  9. children: React.ReactNode;
  10. style?: React.CSSProperties;
  11. prefixCls?: string;
  12. className?: string;
  13. getPopupContainer?: () => HTMLElement;
  14. didUpdate?: (props: PortalProps) => void
  15. }
  16. export interface PortalState {
  17. container: undefined | HTMLElement
  18. }
  19. const defaultGetContainer = () => document.body;
  20. class Portal extends PureComponent<PortalProps, PortalState> {
  21. static contextType = ConfigContext;
  22. static defaultProps = {
  23. // getPopupContainer: () => document.body,
  24. prefixCls: `${BASE_CLASS_PREFIX}-portal`,
  25. };
  26. static propTypes = {
  27. children: PropTypes.node,
  28. prefixCls: PropTypes.string,
  29. getPopupContainer: PropTypes.func,
  30. className: PropTypes.string,
  31. didUpdate: PropTypes.func,
  32. };
  33. el: HTMLElement;
  34. context: ContextValue;
  35. constructor(props: PortalProps, context: ContextValue) {
  36. super(props);
  37. this.state = {
  38. container: this.initContainer(context, true)
  39. };
  40. }
  41. componentDidMount() {
  42. const container = this.initContainer(this.context);
  43. if (container!==this.state.container) {
  44. this.setState({ container });
  45. }
  46. }
  47. initContainer = (context: ContextValue, catchError = false) => {
  48. try {
  49. let container: HTMLElement | undefined = undefined;
  50. if (!this.el) {
  51. this.el = document.createElement('div');
  52. }
  53. if (!this.state?.container) {
  54. this.el = document.createElement('div');
  55. const getContainer = this.props.getPopupContainer || context.getPopupContainer || defaultGetContainer;
  56. const portalContainer = getContainer();
  57. portalContainer.appendChild(this.el);
  58. this.addStyle(this.props.style);
  59. this.addClass(this.props.prefixCls, context, this.props.className);
  60. container = portalContainer;
  61. return container;
  62. }
  63. } catch (e) {
  64. if (!catchError) {
  65. throw e;
  66. }
  67. }
  68. return this.state?.container;
  69. }
  70. componentDidUpdate(prevProps: PortalProps) {
  71. // visible callback
  72. const { didUpdate } = this.props;
  73. if (didUpdate) {
  74. didUpdate(prevProps);
  75. }
  76. }
  77. componentWillUnmount() {
  78. const { container } = this.state;
  79. if (container) {
  80. container.removeChild(this.el);
  81. }
  82. }
  83. addStyle = (style = {}) => {
  84. if (this.el) {
  85. for (const key of Object.keys(style)) {
  86. this.el.style[key] = style[key];
  87. }
  88. }
  89. };
  90. addClass = (prefixCls: string, context = this.context, ...classNames: string[]) => {
  91. const { direction } = context;
  92. const cls = classnames(prefixCls, ...classNames, {
  93. [`${prefixCls}-rtl`]: direction === 'rtl'
  94. });
  95. if (this.el) {
  96. this.el.className = cls;
  97. }
  98. };
  99. render() {
  100. const { state, props } = this;
  101. if (state.container) {
  102. return createPortal(props.children, this.el);
  103. }
  104. return null;
  105. }
  106. }
  107. export default Portal;