index.tsx 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. import React, { CSSProperties } from 'react';
  2. import ReactDOM from 'react-dom';
  3. import cls from 'classnames';
  4. import PropTypes from 'prop-types';
  5. import ConfigContext, { ContextValue } from '../configProvider/context';
  6. import NotificationListFoundation, {
  7. ConfigProps, NotificationListAdapter,
  8. NotificationListProps,
  9. NotificationListState
  10. } from '@douyinfe/semi-foundation/notification/notificationListFoundation';
  11. import { cssClasses, strings } from '@douyinfe/semi-foundation/notification/constants';
  12. import Notice from './notice';
  13. import BaseComponent from '../_base/baseComponent';
  14. import '@douyinfe/semi-foundation/notification/notification.scss';
  15. import NoticeTransition from './NoticeTransition';
  16. import getUuid from '@douyinfe/semi-foundation/utils/uuid';
  17. import useNotification from './useNotification';
  18. import { NoticeInstance, NoticePosition, NoticeProps, NoticeState } from '@douyinfe/semi-foundation/notification/notificationFoundation';
  19. // TODO: Automatic folding + unfolding function when there are more than N
  20. export { NoticeTransitionProps } from './NoticeTransition';
  21. export interface NoticeReactProps extends NoticeProps{
  22. style?: CSSProperties;
  23. }
  24. export {
  25. NoticeState,
  26. NotificationListProps,
  27. NotificationListState,
  28. ConfigProps
  29. };
  30. export type NoticesInPosition = { top: NoticeInstance[];
  31. topLeft: NoticeInstance[];
  32. topRight: NoticeInstance[];
  33. bottom: NoticeInstance[];
  34. bottomLeft: NoticeInstance[];
  35. bottomRight: NoticeInstance[];
  36. };
  37. let ref: NotificationList = null;
  38. const defaultConfig = {
  39. duration: 3,
  40. position: 'topRight' as NoticePosition,
  41. motion: true,
  42. content: '',
  43. title: '',
  44. zIndex: 1010,
  45. };
  46. class NotificationList extends BaseComponent<NotificationListProps, NotificationListState> {
  47. static contextType = ConfigContext;
  48. static propTypes = {
  49. style: PropTypes.object,
  50. className: PropTypes.string,
  51. direction: PropTypes.oneOf(strings.directions),
  52. };
  53. static defaultProps = {};
  54. static useNotification: typeof useNotification;
  55. private static wrapperId: string;
  56. private noticeStorage: NoticeInstance[];
  57. private removeItemStorage: NoticeInstance[];
  58. constructor(props: NotificationListProps) {
  59. super(props);
  60. this.state = {
  61. notices: [],
  62. removedItems: [],
  63. };
  64. this.noticeStorage = [];
  65. this.removeItemStorage = [];
  66. this.foundation = new NotificationListFoundation(this.adapter);
  67. }
  68. context: ContextValue;
  69. get adapter(): NotificationListAdapter {
  70. return {
  71. ...super.adapter,
  72. updateNotices: (notices: NoticeInstance[], removedItems: NoticeInstance[] = []) => {
  73. this.noticeStorage = [...notices];
  74. this.removeItemStorage = [...removedItems];
  75. // setState is async sometimes and react often merges state, so use "this" , make sure other code always get right data.
  76. this.setState({ notices, removedItems });
  77. },
  78. getNotices: () => this.noticeStorage,
  79. };
  80. }
  81. static addNotice(notice: NoticeProps) {
  82. const id = getUuid('notification');
  83. if (!ref) {
  84. const { getPopupContainer } = notice;
  85. const div = document.createElement('div');
  86. if (!this.wrapperId) {
  87. this.wrapperId = getUuid('notification-wrapper').slice(0, 32);
  88. }
  89. div.className = cssClasses.WRAPPER;
  90. div.id = this.wrapperId;
  91. div.style.zIndex = String(typeof notice.zIndex === 'number' ? notice.zIndex : defaultConfig.zIndex);
  92. if (getPopupContainer) {
  93. const container = getPopupContainer();
  94. container.appendChild(div);
  95. } else {
  96. document.body.appendChild(div);
  97. }
  98. ReactDOM.render(React.createElement(NotificationList, { ref: instance => (ref = instance) }), div, () => {
  99. ref.add({ ...notice, id });
  100. });
  101. } else {
  102. ref.add({ ...notice, id });
  103. }
  104. return id;
  105. }
  106. static removeNotice(id: string) {
  107. if (ref) {
  108. ref.remove(id);
  109. }
  110. return id;
  111. }
  112. static info(opts: NoticeProps) {
  113. return this.addNotice({ ...defaultConfig, ...opts, type: 'info' });
  114. }
  115. static success(opts: NoticeProps) {
  116. return this.addNotice({ ...defaultConfig, ...opts, type: 'success' });
  117. }
  118. static error(opts: NoticeProps) {
  119. return this.addNotice({ ...defaultConfig, ...opts, type: 'error' });
  120. }
  121. static warning(opts: NoticeProps) {
  122. return this.addNotice({ ...defaultConfig, ...opts, type: 'warning' });
  123. }
  124. static open(opts: NoticeProps) {
  125. return this.addNotice({ ...defaultConfig, ...opts, type: 'default' });
  126. }
  127. static close(id: string) {
  128. return this.removeNotice(id);
  129. }
  130. static destroyAll() {
  131. if (ref) {
  132. ref.destroyAll();
  133. const wrapper = document.querySelector(`#${this.wrapperId}`);
  134. ReactDOM.unmountComponentAtNode(wrapper);
  135. wrapper && wrapper.parentNode.removeChild(wrapper);
  136. ref = null;
  137. this.wrapperId = null;
  138. }
  139. }
  140. static config(opts: ConfigProps) {
  141. ['top', 'left', 'bottom', 'right'].map(pos => {
  142. if (pos in opts) {
  143. defaultConfig[pos] = opts[pos];
  144. }
  145. });
  146. if (typeof opts.zIndex === 'number') {
  147. defaultConfig.zIndex = opts.zIndex;
  148. }
  149. if (typeof opts.duration === 'number') {
  150. defaultConfig.duration = opts.duration;
  151. }
  152. if (typeof opts.position === 'string') {
  153. defaultConfig.position = opts.position as NoticePosition;
  154. }
  155. }
  156. add = (noticeOpts: NoticeProps) => this.foundation.addNotice(noticeOpts);
  157. remove = (id: string | number) => {
  158. this.foundation.removeNotice(String(id));
  159. };
  160. destroyAll = () => this.foundation.destroyAll();
  161. renderNoticeInPosition = (
  162. notices: NoticeInstance[],
  163. position: NoticePosition,
  164. removedItems: NoticeInstance[] = []
  165. ) => {
  166. const className = cls(cssClasses.LIST);
  167. // TODO notifyOnClose
  168. if (notices.length) {
  169. const style = this.setPosInStyle(notices[0]);
  170. return (
  171. // @ts-ignore
  172. <div placement={position} key={position} className={className} style={style}>
  173. {notices.map((notice, index) =>
  174. (notice.motion ? (
  175. <NoticeTransition key={notice.id || index} position={position} motion={notice.motion}>
  176. {removedItems.find(item => item.id === notice.id) ?
  177. null :
  178. transitionStyle => (
  179. <Notice
  180. {...notice}
  181. style={{ ...transitionStyle, ...notice.style }}
  182. key={notice.id}
  183. close={this.remove}
  184. />
  185. )}
  186. </NoticeTransition>
  187. ) : (
  188. <Notice {...notice} style={{ ...notice.style }} key={notice.id} close={this.remove} />
  189. ))
  190. )}
  191. </div>
  192. );
  193. }
  194. return null;
  195. };
  196. setPosInStyle(noticeInstance: NoticeInstance) {
  197. const style = {};
  198. ['top', 'left', 'bottom', 'right'].forEach(pos => {
  199. if (pos in noticeInstance) {
  200. const val = noticeInstance[pos];
  201. style[pos] = typeof val === 'number' ? `${val}px` : val;
  202. }
  203. });
  204. return style;
  205. }
  206. render() {
  207. let { notices } = this.state;
  208. const { removedItems } = this.state;
  209. notices = Array.from(new Set([...notices, ...removedItems]));
  210. const noticesInPosition: NoticesInPosition = {
  211. top: [],
  212. topLeft: [],
  213. topRight: [],
  214. bottom: [],
  215. bottomLeft: [],
  216. bottomRight: [],
  217. };
  218. notices.forEach(notice => {
  219. const direction = notice.direction || this.context.direction;
  220. const defaultPosition = direction === 'rtl' ? 'topLeft' : 'topRight';
  221. const position = notice.position || defaultPosition;
  222. noticesInPosition[position].push(notice);
  223. });
  224. const noticesList = Object.entries(noticesInPosition).map(obj => {
  225. const pos = obj[0];
  226. const noticesInPos = obj[1];
  227. return this.renderNoticeInPosition(noticesInPos, pos as NoticePosition, removedItems);
  228. });
  229. return <React.Fragment>{noticesList}</React.Fragment>;
  230. }
  231. }
  232. NotificationList.useNotification = useNotification;
  233. export default NotificationList;