index.tsx 9.8 KB

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